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.