This proposal defines new syntax to throw exceptions from within an expression context.
Stage: 2
Champion: Ron Buckton (@rbuckton)
For more information see the TC39 proposal process.
- Ron Buckton (@rbuckton)
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'
++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] `;`
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 inerr.stack
in a host environment.- This can be mitigated in some hosts that have
Error.captureStackTrace
- This can be mitigated in some hosts that have
- 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).
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
- 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).
- Initial specification text.
- Optional. Transpiler support.
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
- 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.