Skip to content

Template literal interpolating a Symbol stringifies instead of throwing TypeError #10609

Description

@proggeramlug

A template literal interpolating a Symbol silently stringifies it, where the spec requires a TypeError. String concatenation on the same value already throws correctly, so this is a gap in the template path specifically, not in ToString.

ToString(argument) (ECMA-262 7.1.17) throws for a Symbol, and template substitution reaches it through GetValueToString in §13.2.8.6. String(sym) and sym.toString() are the two legal ways to stringify one and must keep working.

Reproduction

const s = Symbol("x");
try { console.log(`${s as any}`); } catch (e: any) { console.log("template threw", e.constructor.name + ":", e.message); }
try { console.log("" + (s as any)); } catch (e: any) { console.log("concat threw", e.constructor.name + ":", e.message); }
console.log("String():", String(s));
console.log("toString():", s.toString());
try { console.log(`a${s as any}b`); } catch (e: any) { console.log("multi threw", e.constructor.name + ":", e.message); }

node 26.5.1

template threw TypeError: Cannot convert a Symbol value to a string
concat threw TypeError: Cannot convert a Symbol value to a string
String(): Symbol(x)
toString(): Symbol(x)
multi threw TypeError: Cannot convert a Symbol value to a string

perry (clean main, built at c8cf450563)

Symbol(x)
concat threw TypeError: Cannot convert a Symbol value to a string
String(): Symbol(x)
toString(): Symbol(x)
aSymbol(x)b

Lines 2, 3 and 4 match. Lines 1 and 5 — the two template forms — should throw and do not.

Where to look

The + path throws, so the Symbol check exists and is reachable; the template path does not consult it. Templates desugar to a concat chain, and js_string_concat_chain's classify loop falls back to js_jsvalue_to_string for a pointer-tagged value, which stringifies a Symbol rather than rejecting it. The pairwise + helper rejects first. Worth checking whether the single-substitution form (`${s}`, which lowers to a bare StringCoerce with no chain) and the multi-part form need the check in different places.

Why it matters beyond conformance

Silently stringifying is the worse failure direction: a Symbol used as a key or brand that lands in a template produces a plausible-looking "Symbol(x)" string instead of failing, so the mistake propagates into output, keys or comparisons rather than surfacing where it happened.

Provenance

Found while measuring template-literal interpolation for #10576. Present identically on that PR's before and after binaries, so it is pre-existing on main and unrelated to that change.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions