-
Notifications
You must be signed in to change notification settings - Fork 207
No Nested Await
context: Coding Style
Our general rule (which will be enforced as part of the Jessie linter) is that await
must only appear in the top level of any given function. It should not appear inside of a conditional or a loop.
For example, the following function runs thunk()
(which might or might not be async
), and wants to ensure that the meteringDisabled
counter is decremented afterwards. The non-recommended approach is:
async function runWithoutMeteringAsync(thunk) {
meteringDisabled += 1;
try {
return await thunk(); // NOT RECOMMENDED
} finally {
meteringDisabled -= 1;
}
}
This version has a subtle timing concern. If thunk()
throws synchronously, the await
is bypassed entirely, and control jumps immediately to the finally { }
block. This is made more obvious by holding the supposed return Promise in a separate variable:
async function runWithoutMeteringAsync(thunk) {
meteringDisabled += 1;
try {
const p = thunk(); // if this throws..
return await p; // .. this never even runs
} finally {
meteringDisabled -= 1; // .. and control jumps here immediately
}
}
The recommended approach rewrites this to avoid the await
, and instead uses the Promise's .finally
method to achieve the same thing:
async function runWithoutMeteringAsync(thunk) {
meteringDisabled += 1;
return Promise.resolve()
.then(() => thunk())
.finally(() => {
meteringDisabled -= 1;
});
}
(Note that thunk()
must be called inside a .then
to protect against any synchronous behavior it might have. It would not be safe to use return thunk().finally(...)
, and thunk()
might even return a non-Promise with some bogus .finally
method.)
await
effectively splits the function into two pieces: the part that runs before the await
, and the part that runs after, and we must review it with that in mind (including reentrancy concerns enabled by the loss of control between the two). Using await
inside a conditional means that sometimes the function is split into two pieces, and sometimes it is not, which makes this review process much harder.
We sometimes refer to this rule as "only use top-level await
". Keep in mind that we mean "top of each function", rather than "top level of the file" (i.e. outside of any function body, which is a relatively recent addition to JS, and only works in a module context).
See "Atomic" vs "Transactional" terminology note
Often our intent is to ensure that each method can run in as few turns as possible, but sometimes the simplest way to write a method requires an await
deep in a loop, or inside a try-catch. A work-around takes advantage of a recent improvement to the lint rule that enforces this constraint. As long as the first appearance of await
is at the top level, further await
s are allowed. So you can just write
await null;
early in a method, and this rule doesn't impose further restrictions.
Every async function has a synchronous prelude: when you call an async function, every statement until the first await runs on your stack. An async function has a static synchronous prelude if it has a top-level await before all other awaits.
See #6219