Skip to content

tc39/proposal-throw-expressions

ECMAScript throw expressions

This proposal defines new syntax to throw exceptions from within an expression context.

Status

Stage: 2
Champion: Ron Buckton (@rbuckton)

For more information see the TC39 proposal process.

Authors

  • Ron Buckton (@rbuckton)

Proposal

A throw expression allows you to throw exceptions in expression contexts. For example:

  • Parameter initializers
    function save(filename = throw new TypeError("Argument required")) {
    }
  • Arrow function bodies
    lint(ast, { 
      with: () => throw new Error("avoid using 'with' statements.")
    });
  • Conditional expressions
    function getEncoder(encoding) {
      const encoder = encoding === "utf8" ? new UTF8Encoder() 
                    : encoding === "utf16le" ? new UTF16Encoder(false) 
                    : encoding === "utf16be" ? new UTF16Encoder(true) 
                    : throw new Error("Unsupported encoding");
    }
  • Logical operations
    class Product {
      get id() { return this._id; }
      set id(value) { this._id = value || throw new Error("Invalid value"); }
    }

A throw expression does not replace a throw statement due to the difference in the precedence of their values. To maintain the precedence of the throw statement, we must add a lookahead restriction to ExpressionStatement to avoid ambiguity.

Due to the difference in precedence between a throw expression and a ThrowStatement, certain operators to the right of the expression would parse differently between the two which could cause ambiguity and confusion:

throw a ? b : c; // evaluates 'a' and throws either 'b' or 'c'
(throw a ? b : c); // without restriction would throw 'a', so `?` is forbidden

throw a, b; // evaluates 'a', throws 'b'
(throw a, b); // would throw 'a', not 'b', so `,` is forbidden

throw a && b; // throws 'a' if 'a' is falsy, otherwise throws 'b'
(throw a && b); // would always throw 'a', so `&&` is forbidden

throw a || b; // throws 'a' if 'a' is truthy, otherwise throws 'b'
(throw a || b); // would always throw 'a', so `||` is forbidden

// ... etc.

As a result, all binary operators and the ? operator are forbidden to the right of a throw expression. To use these operators inside of a throw expression, the expression must be surrounded with parentheses:

(throw (a, b)); // evaluates 'a', throws 'b'
(throw (a ? b : c)); // evaluates 'a' and throws either 'b' or 'c'

However, we do not forbid : so that a throw expression can still be easily used in a ternary:

const x = a ? throw b : c; // if 'a' then throw 'b', else evaluate 'c'

Grammar

++ThrowExpressionInvalidPunctuator : one of
  `,` `<` `>` `<=` `>=` `==` `!=` `===` `!==` `+` `-` `*` `/` `%` `**` `<<` `>>` `>>>` `&` `|` `^` `&&` `||` `??`
  `=` `+=` `-=` `*=` `%=` `**=` `<<=` `>>=` `>>>=` `&=` `|=` `^=` `&&=` `||=` `??=` `?`

  UnaryExpression[Yield, Await] :
++  `throw` UnaryExpression[?Yield, ?Await] [lookahead ∉ ThrowExpressionInvalidPunctuator]

  ExpressionStatement[Yield, Await] :
--  [lookahead ∉ {`{`, `function`, `async` [no |LineTerminator| here] `function`, `class`, `let [`}] Expression[+In, ?Yield, ?Await] `;`
++  [lookahead ∉ {`{`, `function`, `async` [no |LineTerminator| here] `function`, `class`, `let [`, `throw`}] Expression[+In, ?Yield, ?Await] `;`

Other Notes

A throw expression can be approximated in ECMAScript using something like the following definition:

const __throw = err => { throw err; };

// via helper...
function getEncoder1(encoding) {
  const encoder = encoding === "utf8" ? new UTF8Encoder() 
                : encoding === "utf16le" ? new UTF16Encoder(false) 
                : encoding === "utf16be" ? new UTF16Encoder(true) 
                : __throw(new Error("Unsupported encoding"));
}

// via arrow...
function getEncoder2(encoding) {
  const encoder = encoding === "utf8" ? new UTF8Encoder() 
                : encoding === "utf16le" ? new UTF16Encoder(false) 
                : encoding === "utf16be" ? new UTF16Encoder(true) 
                : (() => { throw new Error("Unsupported encoding"); })();
}

However, this has several downsides compared to a native implementation:

  • The __throw helper will appear in err.stack in a host environment.
    • This can be mitigated in some hosts that have Error.captureStackTrace
  • Hosts require more information for optimization/deoptimization decisions as the throw is not local to the function.
  • Not ergonomic for debugging as the frame where the exception is raised is inside of the helper.
  • Inline invoked arrow not ergonomic (at least 10 more symbols compared to native).

Resources

TODO

The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:

Stage 1 Entrance Criteria

  • Identified a "champion" who will advance the addition.
  • Prose outlining the problem or need and the general shape of a solution.
  • Illustrative examples of usage.
  • High-level API (proposal does not introduce an API).

Stage 2 Entrance Criteria

Stage 3 Entrance Criteria

Stage 4 Entrance Criteria

  • Test262 acceptance tests have been written for mainline usage scenarios and merged.
  • Two compatible implementations which pass the acceptance tests: [1], [2].
  • A pull request has been sent to tc39/ecma262 with the integrated spec text.
  • The ECMAScript editor has signed off on the pull request.