emberjs/ember.js · error

${node.path.type} "${node.path.type === 'StringLiteral' ? no

Error message

${node.path.type} "${node.path.type === 'StringLiteral' ? node.path.original : value}" cannot be called as a sub-expression, replace (${value}) with ${value}

What it means

In a call/sub-expression position like {{value}} the path must be an identifier-based path, not a Handlebars literal. When node.path is a StringLiteral, NumberLiteral, BooleanLiteral, UndefinedLiteral or NullLiteral, the parser cannot treat the literal as a callable path and throws, suggesting to drop the parentheses (i.e. use the literal directly, not as a call).

Source

Thrown at packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts:635

    case 'StringLiteral':
    case 'UndefinedLiteral':
    case 'NullLiteral':
    case 'NumberLiteral':
    case 'BooleanLiteral': {
      let value: string;
      if (node.path.type === 'BooleanLiteral') {
        value = node.path.original.toString();
      } else if (node.path.type === 'StringLiteral') {
        value = `"${node.path.original}"`;
      } else if (node.path.type === 'NullLiteral') {
        value = 'null';
      } else if (node.path.type === 'NumberLiteral') {
        value = node.path.value.toString();
      } else {
        value = 'undefined';
      }
      throw generateSyntaxError(
        `${node.path.type} "${
          node.path.type === 'StringLiteral' ? node.path.original : value
        }" cannot be called as a sub-expression, replace (${value}) with ${value}`,
        compiler.source.spanFor(node.path.loc)
      );
    }
  }

  const params = node.params.map((e) => compiler.acceptNode<HBS.Expression['type']>(e));

  // if there is no hash, position it as a collapsed node immediately after the last param (or the
  // path, if there are also no params)
  const end = isPresentArray(params) ? getLast(params).loc : path.loc;

  const hash = node.hash
    ? compiler.Hash(node.hash)
    : b.hash({
        pairs: [],

View on GitHub (pinned to 26f97246a8)

Solutions

  1. Remove the parentheses and use the literal directly: ("foo") → "foo", (42) → 42, (true) → true.
  2. If you meant to call a helper, put an identifier path in the call position and pass literals as arguments: {{uppercase "foo"}} instead of {{("foo")}}.
  3. Fix the generator/macro that places literals in the path position of call nodes.

Example fix

// before
{{("hello")}}

// after
{{"hello"}}
// or, if a helper call was intended:
{{uppercase "hello"}}
Defensive patterns

Strategy: validation

Validate before calling

// reject literal paths inside parenthesized sub-expressions
function assertNoLiteralCalls(template) {
  const bad = template.match(/\(\s*("[^"]*"|'[^']*'|\d+|true|false|null|undefined)\s*\)/g);
  if (bad) throw new Error(`Literals cannot be called as sub-expressions: ${bad.join(', ')}`);
}
assertNoLiteralCalls(template);

Prevention

When it happens

Trigger: Invoking the parser with a mustache or sub-expression whose path is a literal — e.g. {{("foo")}}, {{(42)}}, {{(true)}} — or passing {{123}} where the visitor's acceptCallNodes (called with {path, params, hash}) detects isHBSLiteral(node.path). The message interpolates the literal's type and value.

Common situations: Programmatic template generation that wraps values in parentheses assuming expression syntax; confused template syntax where a literal was meant to be passed as a param but got placed in the path slot; copy/paste from languages where ("str") is a valid expression.

Related errors


AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01). Data as JSON: /api/errors/38d7dbca9d36f071. Report an issue: GitHub.