Synchronous exceptions thrown from complex expressions create abandoned promises. Solutions?

I just realised that Promise.all does not take a list of promises. It takes an iterable of promises. Which means it is possible already to write

try {
  const [a, b, c] = await Promise.all(function*() {
    yield new Promise((_, reject) => setTimeout(reject, 1, "first"));
    yield Promise.reject("second");
    throw new Error("third");
  }());
} catch(e) {
  console.error(e);
}

which does not cause an unhandled rejection. I have to admit it's totally ugly, but it seems to have exactly the semantics you asked for?

1 Like

I'm a huge fan of the article Timeouts and cancellation for humans and the design laid out in it.
It allows to create a scope in which tasks (asynchronous functions) can be spawned, and the scope basically doesn't exit until all tasks in it have completed (or were cancelled). But I don't think I should attempt to explain the concept in the here, go ahead and read the article! It's really worth it, even for people not working with Python.

Not quite the same as the Promise.all case I'm citing where multiple promises reject. In that case, all rejections have handlers installed, but the handlers are themselves collected or something and so I get unhandled rejection errors despite that. Need to test this a little deeper, though.

It seems to work :-) -- I guess because Promise.all() attaches catch handlers synchronously as it consumes promises from the generator. So this is sort of how the "asynchronous comma" could be implemented by e.g. Babel -- but the syntax is, as you say, hardly workable.

Nice design indeed, but seems to be reliant on the ole' "everyone uses this facility" rule? So something like this can only be incorporated at a fundamental level rather than used incrementally?

Also, I admit I speed-read the article so may have missed a few -- or a lot -- of ideas... but offhand I don't see how this cancellation mechanism can be readily applied to the Promise.all() problem -- cancel all underlying asynchronous operations whose promises have not yet settled at the point where the first one failed?

Alright, I think we're going somewhere with your await[] syntax :) - I have a more clear idea of what we're tackling now.

What we're looking for is a syntax that can be used to ensure no uncaught promise errors will slip through between the time that you make the promise and the time you await.

The newly proposed array[] syntax would make great strides towards that direction, but I do see a couple of limitations to it. For these bullet points, I'll refer to each comma-separated expression in your await list as a promised expression.

  • What do you do if one of the promised expressions depends on the result of another promised expression to evaluate?
  • How would you handle complicated result-handling logic? i.e. how might you translate something like this to this new syntax, in an error-safe way? await Promise.all([<expression 1 that might throw>, Promise.race([<expression 2 that might throw>, <expression 3>])])

I think both of these scenarios could be resolved if we could name each promised expression. Here's an alternative syntax that allows naming the expressions by just binding each expression to a variable.

The following "promise declaration" syntax works as follows:

// The following statement
async const promisedValue = <expression>
// is equivalent to any of these that have already been brought up in this thread.
const promisedValue = promise <expression>
const promisedValue = async do { <expression> }
const promisedValue = Promise.resolve().then(() => <expression>)

The beauty comes is when you chain the declarations:

async const
  promisedValue1 = getValue1(),
  promisedValue2 = getValue2(ThisMightThrow(), await promisedValue1),
  promisedValue3 = getValue3(...await Promise.all([promisedValue1, promisedValue2])),
  promisedResults = await Promise.all([promisedValue1, promisedValue2, promisedValue3])

const [value1, value2, value3] = await promisedResults

This sort of makes a promise pipeline, where it's very easy to construct new promises that depend on the results of earlier ones. One important thing to note is that no errors can be made within that async const declaration - any errors would get converted to rejected promises. Thus, as long as this syntax is used properly, it's very easy to ensure that we're not creating a promise then throwing an error before the promise is awaited. The equivalent to the above example would probably be something like this:

const getValue2Param = ThisMightThrow()
const promisedValue1 = getValue1()
const promisedValue2 = promisedValue1.then(value1 => getValue2(getValue2Param, value1))
const [value1, value2, value3] = await Promise.all([
  promisedValue1,
  promisedValue2,
  Promise.all([promisedValue1, promisedValue2]).then(res => getValue3(...res))
])

Notice how the ordering of statements in this rewrite is much more fragile. In this version, I had to move the function call to thisMightThrow() above the location where the first promise was made. The reasons for moving it above aren't immediately apparent by looking at this code, and it would be so easy for future maintainers to move it below the first promise, creating a bug. This doesn't need to happen with this promise declaration syntax, because the errors are automatically swallowed up and turned into rejected promises, so thisMightThrow() can just be called at the place where its value gets used.

Here are some more examples based on previous code snippets from this thread. For clarity, I'm putting a "$" at the end of any variable that holds a promise.

async fetchUserInfo(params) {}
  try {
    async const
      value1$ = await getValue1(),
      value2$ = await getValue2(params),
      value3$ = await getValue3(await value1$),
      userInfo$ = do {
        const [value1, value2, value3] = await Promise.all([value1$, value2$, value3$])
        { value1, value2, value3 }
      }

    return await userInfo$
  } catch (err) {
    if (err.type === NOT_LOGGED_IN) return null
    throw err
  }
}
async function compareApiImplementations(parameters) {
  async const
    result1$ = newImplementation(parameters),
    result2$ = oldImplementation(adaptParametersToOldImplementation(parameters)),
    comparison$ = compareResults(...await Promise.all([result1$, result2$])
  return comparison$
}

This example shows how one might implement the following snippet from earlier, in an error safe way: await Promise.all([<expression 1 that might throw>, Promise.race([<expression 2 that might throw>, <expression 3>])])

async const
  part1 = <expression 1 that might throw>,
  part2 = <expression 2 that might throw>,
  result = await Promise.all([part1, Promise.race([part2, <expression 3>])])

return await result

And this example shows the similarities this has with await[]:

// The following example with await[] ...
const [res1, res2] = await[
  <expression1>,
  <expression2>
].all()

// ... would be equivalent to this:
async const
  res1$ = <expression1>,
  res2$ = <expression2>

const [res1, res2] = await Promise.all([res1$, res2$])

There are certainly some flaws I can think of with this concept, but I think it's at least a step in the right direction for solving @MaxMotovilov's issue.

Let's parse this: call them A and B; "A depends on B" I interpret as B is a sub-expression of A. After all, that's how traditional expressions work: in 2 * (3 + 4), 2 * () depends on (3+4). In case of async expressions, "A depends on the result of B" must mean "await B" is a sub-expression of A. I don't think this even falls under the same use case.

await[ expr1, await[ expr2, expr3 ].race() ].all()

Seems straightforward to me.

Being rather allergic to throwaway name bindings, I fail to see the beauty of the "async declaration", but I think it is, indeed, equivalent to my original "unary operator". Depending on the context, one is either more or less verbose than the other: if you really need the bindings, your syntax saves on not having to repeat the "async" qualifier in front of the declaration; if you don't need the bindings at all, my syntax wins by omitting the unnecessary names.

The trick in all cases -- including await syntax -- is having a recognizable syntactic context where any evaluation result including a synchronous exception is converted to a promise. Async functions make that guarantee at the scope of a function; I'd like to have it at the scope of a [sub-]expression.

BTW yet another variation of await -- let me call it async just to keep the two distinct -- could simply make this guarantee for a list of sub-expressions and return an array of promises. This would lead to simpler semantics and an easier fit with the existing facilities at a price of, again, verbosity :-(

await Promise.all( async[ foo(), bar() ] )

We can also explore the idea @bergus described in Synchronous exceptions thrown from complex expressions create abandoned promises. Solutions? - #41 by bergus. What it amounts to is a generator-based interface where the consumer of the sequence has a chance to perform [synchronous] actions on each element before the next element will be evaluated. Missing is a simple syntax that would allow construction of sequences in that manner -- a form of not-quite-iterator comprehension from Python 3. I am going to take advantage of the fact that JS does not have an unary "*" operator (oh the fun I used to have in C++ overloading it!):

await Promise.all( *[ foo(), bar() ] );

And now I posit the semantics of "*[" list-of-expressions "]" as simply:

(function*(){ yield expression-1; ... yield expression-n; })();

It has nothing to do with async at all -- just a way to construct generators in-line; a "generator comma" instead of an "async comma". This means that Promise.all() and other possible async combinators have to all be tricky enough to consume each promise atomically but that's a worry for another day. It is sufficient that we can guarantee sub-expression isolation that way.

I guess this is now my favorite option, hah!

Follow-up: I can't seem to find an existing proposal for a generator literal -- because this is what it is. Given that it solves a real problem with async expressions, and that the Promise.all()/Promise.race() APIs seem custom-made for such a thing, perhaps there should be one?

In its simplest form, *[ expression_1, ... expression_N ] is evaluated as

(function*() {
  yield expression_1;
  ...
  yield expression_N;
})()

As there's no way to inject local names in the closure created by the function*() wrapper, I fail to see any possible edge cases. Should be really simple to implement with a Babel plugin (I may try doing just that shortly, if I find some time).

Now that I'm in a celebratory mood, I feel ready for some mischief. How about supporting the equivalent of yield* in this syntax? After all, it's just the same as ... spread operator in an array literal, right? So let's say "*[ ...expression ]" -- this time "..." is used verbatim -- is an equivalent of:

(function*() {
  yield* expression;
})()

So far so good, right? Still nothing breaks, not really any new syntax introduced as ... already works inside both [] and {}, so why not here.

Digging further. When we say:

const seq = *[ expr1, expr2, ...expr ];

at what point is "seq" bound? The question is not trivial because none of expr1, expr2 or expr are even evaluated until someone starts iterating over seq! The natural answer is that seq is bound to the generator roughly speaking at "= *[". Fair enough.

Here comes the prank I was carefully preparing:

let i = 0;
const cardinals = *[ ++i, ...cardinals ];

I give you... infinite recurrent sequences! Just what I always wanted for my birthday!

let a=0, b=1;
const fib = *[a+=b, b+=a, ...fib];
2 Likes

Related: GitHub - tc39/proposal-await.ops: Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises which adds await.all exp syntax

Ah - I had wrongly presumed that the goal was to put all of the async expressions into a single await[], and as long as we do that, we can be sure no errors happen between the time a promise is made, and when it gets used. But, I see now that its perfectly safe to nest them, or to assign the result of one await to a variable and immidietally use it in another.

We can just ignore my whole async const thing - there were a number of things I didn't like about it anyways. I was just trying to find a way to do it all in one syntax chunk. You'll probably get better luck with your *[] syntax.

I guess the other options would be looking at @aclaymore's reference to this other proposal and seeing about altering it slightly.

So, instead of await.all <array>, it uses await.all[<lazy-evaluated expression 1, lazy-evaluated expression 2>]

This would make these await.* syntax additions do something other than just provide a shortand for Promise.* functions. And would bring it pretty close to your await[] idea.

Honestly, I now like the generator literal a lot more. It is a simpler -- almost trivial, pure syntax! -- addition, it generalizes into other unrelated capabilities such as by-name parameters and it's far past time for Javascript to start catching up with Python in iterator-based programming.

Trying to find a time window for Babel implementation and a more detailed writeup, so that I can submit this proposal on its own merits.

I've got a better solution here: Proposal + seeking champion: Composable promise concurrency management

Read through - while I see the advantages of the proposal as regards the patterns outlined in it, I fail to see how it applies to this thread. Granted an explicit async wrapper required by your API, even this naive boilerplate solves the problem:

Promise.all([
  foo(), // The async function call
  (async () => bar(baz()))() // Sync call followed by async call, wrapped
])

Verbose, but no more so than it'll have to be to use Promise.scheduleAndRun(), no?