Skip to content

tc39/proposal-throw-expressions

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.