ECMAScript proposal: do expressions

This proposal has preliminary spec text.

Status

This proposal is in stage 1 of the TC39 process.

Motivation

  • expression-oriented programming one of the great advances of FP
  • expressions plug together like legos, making more malleable programming experience in-the-small

Examples

Write in an expression-oriented style, scoping variables as locally as possible:

let x = do {
  let tmp = f();
  tmp * tmp + 1
};

Use conditional statements as expressions, instead of awkward nested ternaries:

let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

Especially nice for templating languages like JSX:

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

Limitations

Because of the potential for confusion, you can't end a do-expression with a declaration, an if without an else, or a loop (even when nested in other statements). For example, the following are all Early Errors:

(do {
  let x = 1;
});
(do {
  function f() {}
});
(do {
  while (cond) {
    // do something
  }
});
(do {
  if (condition) {
    while (inner) {
      // do something
    }
  } else {
    42;
  }
});
(do {
  label: {
    let x = 1;
    break label;
  }
});
(do {
  if (foo) {
    bar
  }
});

More formally, the completion value of the StatementList can't rely on the completion value of a loop or declaration. See EndsInIterationOrDeclaration in the proposed specification for details.

The restriction on declarations is particularly unfortunate. It arises from the fact that declarations have ~empty~ as their completion value, meaning do { 'before'; let x = 'after'; } would evaluate to before. I'd like to pursue changing the completion value for declarations in general, which would affect existing code relying on eval.

Edge cases

var declarations

var declarations are allowed (except as the final statement), with the binding hoisting to the containing function's scope. Exception: var declarations are an Early Error when the do occurs in a parameter expression.

Empty do

do {} is allowed and is equivalent to void 0.

await/yield

The ability to use await and yield is inherited from the context of the enclosing function, as it is in any other expression.

throw

Works fine. Does what you expect.

break/continue/return

These are allowed when in an appropriate context: return is allowed when the do is within a function, break is allowed when within a loop or a switch case, etc. This allows you to write code like this:

function getUserId(blob) {
  let obj = do {
    try {
      JSON.parse(blob)
    } catch {
      return null; // returns from the function
    }
  };
  return obj?.userId;
}

Exceptions

Because of the potential for confusion, unlabeled break and continue are not allowed within the head of a loop, whether or not the loop is within another loop.

return is allowed even within function parameter lists, as in function f(x = do { return null; }) {}. It is not allowed in computed property names in class bodies.

Conflict with do-while

do expressions are prohibited in contexts in which statements are legal. In such contexts you can just use a normal block or enclose the do expression in parentheses.

B.3.3 function hoisting

Sloppy-mode function hoisting is not allowed to pass through a do-expression.