Chapter 4 — async/await Traps
The syntax reads like synchronous code; the promises underneath do not behave like it
Part 1 of the async core. Prerequisites: Chapter 3 — Promises in Real Code (promise states, then chains, unhandled rejections) and the microtask queue from FE-10. Next: Chapter 5 — Race Conditions, which is what most of these traps turn into under load.
How to Read This Chapter
async/await is a thin layer over promises. The code looks sequential, so reviewers read it as sequential, and that is where the bugs hide. Every trap in this chapter comes from one gap: the code on the page is in one order, but it runs as a chain of microtasks, and other code can run between them.
Read Part 1 carefully. Every later part is a consequence of it. For each trap, practise saying the mechanism in one sentence, because in the interview you will find the bug in a file you did not write and then have to explain it out loud.
| Term | Meaning |
|---|---|
| Continuation | The rest of an async function after an await. It runs later, as a microtask. |
| Yield point | Any await. The function pauses there, and other code may run before it resumes. |
| Floating promise | A promise nobody awaits, returns, or attaches .catch to. Its rejection has nowhere to go. |
| Serial awaits | Independent operations awaited one after another, so their latencies add up. |
| Fan-out | Starting many operations at once, usually Promise.all(items.map(fn)). |
| Fail-fast | Promise.all rejects on the first rejection. The other operations keep running. |
| Overlapping run | A timer or handler fires again while the previous async run is still in progress. |
| Top-level await (TLA) | await at module scope in an ES module. The module, and every module that imports it, waits. |
| Static factory | static async create(): an async function that does the async setup and then calls a synchronous constructor. |
Part 1: What await Desugars To
An async function runs synchronously until its first await. At that await, it returns a pending promise to its caller. The rest of the function is registered as a callback on the awaited value, and runs as a microtask once that value settles.
async function load() {
console.log('2. load starts, runs synchronously');
const user = await 42; // even a non-promise yields
console.log('4. after await, in a microtask');
return user;
}
console.log('1. before call');
const p = load();
console.log('3. call returned', p instanceof Promise);
p.then((v) => console.log('5. resolved', v));
Output, in Node 22:
1. before call
2. load starts, runs synchronously
3. call returned true
4. after await, in a microtask
5. resolved 42
Roughly, this function:
async function getBalance(userId) {
const row = await db.query('SELECT balance FROM wallets WHERE user_id = $1', [userId]);
return row.balance;
}
behaves like this one:
function getBalance(userId) {
return new Promise((resolve, reject) => {
Promise.resolve(db.query('SELECT balance FROM wallets WHERE user_id = $1', [userId]))
.then((row) => resolve(row.balance), reject);
});
}
Three facts fall out of that, and the rest of this chapter uses all three:
- Every
awaitis a yield point. Between the line before it and the line after it, any other ready callback, request handler or timer can run. Chapter 5 is about what that does to shared state. awaitalways yields, even on a value that is not a promise.await 42still defers the rest of the function to a microtask.- An async function never throws to its caller. A
throwanywhere inside it, even before the firstawait, becomes a rejection of the returned promise. If the caller does not await or.catchthat promise, the error is lost or crashes the process (Part 6).
Step through the same ordering in the event-loop sim. Predict each line before you press next.
Part 2: Array Methods Do Not Wait
forEach, map, filter, some and reduce call your callback and use its return value. An async callback returns a promise. None of these methods were written to await anything, so each one does something different with that promise, and all of them are wrong in their own way.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const charged = [];
async function chargeAll(ids) {
ids.forEach(async (id) => {
await sleep(10);
charged.push(id);
});
console.log('done, charged =', charged.length);
}
await chargeAll([1, 2, 3]);
await sleep(50);
console.log('later, charged =', charged.length);
done, charged = 0
later, charged = 3
forEach started three async callbacks, threw away the three promises, and returned undefined. chargeAll logged "done" while all three were parked at their first await. await chargeAll(...) resolved with nothing still running in its scope. Any error inside the callbacks becomes an unhandled rejection.
await sleepWhat each array method does with an async callback:
| Code | What you get | Why |
|---|---|---|
arr.forEach(async …) | undefined, immediately | The return value is ignored. |
arr.map(async …) | An array of promises | map stores whatever the callback returns. You must await Promise.all(...) it. |
arr.filter(async …) | Every element kept | A promise is an object, and objects are truthy. |
arr.some(async …) | true for any non-empty array | Same reason. every returns true too. |
arr.reduce(async (accP, x) => (await accP) + x, 0) | The right answer, serially | Each step awaits the previous one. It works, but it is a slow for…of in disguise. |
arr.sort(async …) | The array, unsorted | The comparator returns a promise; sort converts it to a number, gets NaN, and treats every pair as equal. |
The filter case is the one people miss in review:
const users = [{ id: 1, active: false }, { id: 2, active: true }];
const isActive = async (u) => u.active;
users.filter(async (u) => await isActive(u)).length; // 2, both kept
const flags = await Promise.all(users.map(isActive)); // [false, true]
users.filter((_, i) => flags[i]).map((u) => u.id); // [2]
The three correct shapes:
| You want | Write | Note |
|---|---|---|
| One at a time, in order | for (const id of ids) await charge(id); | Use when order matters, or each step depends on the last. |
| All at once | await Promise.all(ids.map(charge)); | Only when the list is small and bounded (Part 4). |
| At most K at a time | A pool (mapLimit below; full version in Chapter 7) | The default for anything driven by user input or a queue. |
Part 3: Accidental Serial Awaits
The opposite mistake. Each line below waits for the previous one, although none of them needs the previous result:
const user = await getUser(id); // 100 ms
const plan = await getPlan(id); // 100 ms
const usage = await getUsage(id); // 100 ms
// about 300 ms in total
const [user, plan, usage] = await Promise.all([getUser(id), getPlan(id), getUsage(id)]);
// about 100 ms in total
Measured in Node 22 with three 100 ms timers: 305 ms serial, 103 ms with Promise.all. On an admin dashboard with eight independent queries, that is the difference between 80 ms and 640 ms.
Serial is correct when a later call needs an earlier result, when order matters (ledger entries), or when you are deliberately keeping load off a rate-limited API. Say which one it is in the review. "These three don't depend on each other" is the whole argument for changing it.
Compare the two shapes in the sim. Then raise the task count and see why "all at once" is not the end of the story.
The "start early, await later" trap
Someone reads the advice above and writes this instead:
async function loadDashboard() {
const userP = getUser(); // 100 ms
const planP = getPlan(); // rejects after 10 ms
try {
const user = await userP; // planP rejects while we wait here
const plan = await planP;
return { user, plan };
} catch (err) {
return { error: err.message };
}
}
It looks equivalent to Promise.all. It is not. planP rejects at 10 ms. At that moment nothing has attached a handler to it, because the function is still parked on await userP. Node's unhandled-rejection check runs after the microtask queue drains, sees a rejected promise with no handler, and crashes the process with exit code 1. The try/catch never gets a chance. This was verified on Node 22. Promise.all attaches a handler to every input at once, which is why it does not have this problem.
Part 4: Unbounded Promise.all Over User Input
app.post('/api/collections/:id/embed', async (req, res) => {
const docs = await db.docs.findMany({ where: { collectionId: req.params.id } });
const vectors = await Promise.all(docs.map((d) => embed(d.text)));
await db.vectors.insertMany(vectors);
res.json({ count: vectors.length });
});
Promise.all does not start anything. docs.map does: it calls embed once per document, all in the same synchronous loop, before Promise.all even sees the array. A collection with 4,000 documents means 4,000 HTTP requests opened in one tick.
What happens next depends on what is downstream:
| Downstream | Failure |
|---|---|
| An LLM or embedding API | Most requests come back 429. If the client retries, it retries all of them at once (the retry storm in Chapter 7). |
A pg pool of size 10 | 10 queries run; the other 3,990 queue inside the pool. Other requests to the same instance wait behind them and hit connectionTimeoutMillis. |
| Memory | 4,000 request bodies and response buffers are alive at once. |
| Fail-fast | One 429 rejects the whole Promise.all, and the other 3,999 calls keep running and billing (Part 5). |
The fix has two parts: cap the input size at the API boundary, and cap concurrency.
export async function mapLimit(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++; // claimed synchronously, so no two workers take the same index
results[i] = await fn(items[i], i);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return results;
}
const vectors = await mapLimit(docs, 8, (d) => embed(d.text));
next++ is safe without a lock because it runs synchronously between yield points. No other worker can run between reading next and incrementing it. Chapter 5 explains why that is the key fact about races in JavaScript.
In the interview: "map starts every call at once, so the concurrency is set by the size of the user's input. I'd put a pool limit on it and a maximum size on the request."
Part 5: Promise.all Fails Fast; the Other Work Does Not Stop
async function task(name, ms, fail) {
await sleep(ms);
if (fail) throw new Error(name + ' failed');
console.log(name, 'finished and wrote its result');
}
try {
await Promise.all([task('A', 10, true), task('B', 50), task('C', 100)]);
} catch (e) {
console.log('caught:', e.message);
}
caught: A failed
B finished and wrote its result
C finished and wrote its result
A promise is a receipt for work, not a handle that can stop it. Promise.all rejected at 10 ms, the caller handled the error and moved on, perhaps returned a 500, and B and C kept running. If they write rows, send emails, or call a paid API, those side effects still happen, after the caller has reported failure.
Two fixes, for two different goals:
| Goal | Use |
|---|---|
| "I want every result, failures included" | Promise.allSettled, then inspect each { status, value | reason }. |
| "If one fails, stop the rest" | Pass one AbortSignal to every task and abort it on the first failure. |
The sim shows both side by side: Promise.all rejecting while its lanes keep going, and allSettled with an AbortController.
Build: allOrAbort(tasks)
Spec. Takes an array of functions (signal) => Promise. Resolves with the results in input order. On the first rejection, it aborts a shared AbortSignal with that error as the reason, so the other tasks can stop, and rejects with the original error. A task that throws synchronously counts as a rejection. An empty array resolves to [].
Reference solution
export function allOrAbort(tasks) {
const controller = new AbortController();
return Promise.all(
tasks.map((task) =>
Promise.resolve()
.then(() => task(controller.signal)) // a sync throw becomes a rejection
.catch((err) => {
controller.abort(err); // tell the siblings to stop
throw err;
}),
),
);
}
Tested with node:test: results come back in order; on a failure at 10 ms, two 100 ms siblings that listen to the signal both see signal.reason equal to the original error and never finish; a synchronous throw is a rejection; [] resolves to [].
Edge cases interviewers check:
- The abort only helps if each task passes
signaldown tofetch, to its sleeps, and to its own loops (Chapter 6). - The returned promise rejects immediately. The siblings are still winding down at that moment. If you need "rejected and everything has stopped", await
Promise.allSettledon the wrapped promises before rethrowing. controller.abort(err)is a no-op after the first call, so the reason is always the first error.- An external caller signal should be combined with
AbortSignal.any([callerSignal, controller.signal]).
Part 6: try/catch Only Catches What You Await
async function save() { throw new Error('db down'); }
function handler() {
try {
save(); // not awaited
} catch (e) {
console.log('caught?', e.message);
}
console.log('handler returned normally');
}
handler();
This prints handler returned normally, then Node prints the stack for Error: db down and exits with code 1. save() did not throw. It returned a rejected promise (Part 1, fact 3). The try block finished synchronously before anyone looked at that promise. When the microtask queue drained with a rejected, unhandled promise, Node raised unhandledRejection. Since Node 15 the default for that is to crash.
The same bug in other shapes:
try { items.forEach(async (i) => await process(i)); } catch {} // catches nothing
try { setTimeout(async () => { await flush(); }, 0); } catch {} // catches nothing
try { return somethingAsync(); } catch {} // Part 7
When you really want fire-and-forget (an analytics event, a cache warm-up), say so in the code and give the error a place to go:
void sendWelcomeEmail(user).catch((err) => logger.warn({ err }, 'welcome email failed'));
The void tells the reader, and the no-floating-promises lint rule, that dropping the promise is deliberate. The .catch is what actually stops the crash.
Part 7: return vs return await Inside try
const fail = () => new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 10));
async function a() {
try { return fail(); }
catch (e) { return 'fallback'; }
finally { console.log('a finally runs'); }
}
async function b() {
try { return await fail(); }
catch (e) { return 'fallback'; }
finally { console.log('b finally runs'); }
}
await b() gives 'fallback'. await a() rejects with timeout, and the catch block never runs.
The mechanism: return fail() leaves the try block at once, carrying a pending promise. The async function's own promise is then resolved with that pending promise and follows it. By the time fail() rejects, a has already left try, catch and finally. return await fail() waits inside the try, so the rejection is thrown there and catch sees it.
The finally version is worse, because it releases resources too early:
async function withConnection() {
const conn = await pool.connect();
try {
return conn.query('SELECT …'); // query still running
} finally {
conn.release(); // runs now, before the query finishes
}
}
A verified version with a flag in place of the connection prints query sees lock held? true, then released, then query done, lock held? false. The connection goes back to the pool while the query is still using it, so the next borrower gets a connection that is in the middle of someone else's query.
Where the return is | return p vs return await p |
|---|---|
Inside try with catch | Different: only return await lets catch see the rejection. |
Inside try with finally, or a using block | Different: only return await keeps the resource alive until p settles. |
| Anywhere else | Same result. return await gives a slightly better async stack trace. |
Lint rule: @typescript-eslint/return-await with "in-try-catch", which requires return await exactly where it matters.
Part 8: Async Functions as Event Handlers and Timer Callbacks
setInterval, EventEmitter, Express 4 and DOM events all call your function and ignore what it returns. When the function is async, two things are lost: the caller cannot wait for it to finish, and the caller cannot see its errors.
Overlapping runs
setInterval(async () => {
const jobs = await claimDueJobs();
for (const job of jobs) await runJob(job);
}, 1000);
setInterval fires every 1000 ms, whether or not the previous run has finished. If one run takes 2.5 s, three runs are in flight at once. All three can claim and run the same jobs, unless claimDueJobs is atomic in the database. A test with a 250 ms task on a 100 ms interval gave { runs: 9, maxRunning: 3 } in one second.
claimDueJobs() → job 7The fix is to schedule the next run from the end of the previous one:
function startPolling(fn, ms) {
let timer;
let stopped = false;
const tick = async () => {
try { await fn(); }
catch (err) { logger.error({ err }, 'poll failed'); } // one failure does not kill the loop
if (!stopped) timer = setTimeout(tick, ms);
};
timer = setTimeout(tick, ms);
return () => { stopped = true; clearTimeout(timer); };
}
The same test gives { runs: 3, maxRunning: 1 }. With more than one process, you still need the database to make the claim atomic (FOR UPDATE SKIP LOCKED, Chapter 5).
Lost errors
| Caller | What happens to a rejected async handler |
|---|---|
setInterval / setTimeout | Unhandled rejection; Node exits. |
EventEmitter.on | Unhandled rejection, unless the emitter was created with { captureRejections: true }, which routes it to the 'error' event. |
| Express 4 route handler | Unhandled rejection: by default Node exits. If an unhandledRejection hook only logs, the request hangs until the client times out. Express 5 catches a rejected promise and passes it to next(err). |
DOM addEventListener, React onClick | Logged as Uncaught (in promise) in the console. The UI may be stuck in a "loading" state. |
stream.on('data', async …) | No backpressure: the stream keeps emitting while your handlers pile up (Chapter 9). |
A click handler is also an overlapping-run problem: two quick clicks start two async runs. That is the double-submit race in Chapter 5.
Part 9: Top-Level Await
In an ES module you can await at the top level. The module's evaluation pauses there, and every module that imports it, directly or indirectly, waits before its own body runs.
// config.mjs
console.log('config: start');
await new Promise((r) => setTimeout(r, 200));
export const config = { model: 'x' };
console.log('config: loaded');
// main.mjs
import { log } from './logger.mjs';
import { config } from './config.mjs';
log('main: body runs, config =', config.model);
Modules that do not depend on config.mjs (here, logger.mjs) still evaluate. main.mjs waits 200 ms.
| Trap | What happens (verified on Node 22) |
|---|---|
| A TLA that never settles (a connection that hangs) | Node exits with code 13 and Warning: Detected unsettled top-level await. In a server, startup just stops. |
require() of an ESM module that uses TLA | Throws ERR_REQUIRE_ASYNC_MODULE. Node 22 can require ES modules, but not ones that use TLA. |
| TLA in a library module | Every consumer's startup now waits on your network call, and they cannot catch its error. |
| TLA in a module the HTTP server imports | listen() runs only after it settles, so a slow call there is slow startup, and health checks fail meanwhile. |
Good uses: one-off scripts, and the entry module of a server loading config or connecting before listen(). Bad use: anything another module imports.
Part 10: Async Constructors
A constructor cannot be async. class A { async constructor() {} } is a SyntaxError: Class constructor may not be an async method. The two usual workarounds are both bad:
- Return a promise from the constructor.
newreturns whatever object a constructor returns, sonew Store()is now a promise:instanceof Storeisfalse, and every caller must rememberawait new Store(). - Start
this.connect()without awaiting it. The object is handed out before it is ready, and the connect promise is floating, so its failure crashes the process.
Use a static factory. All the async work happens before the constructor, and the constructor only assigns fields:
class VectorStore {
#pool;
constructor(pool) { this.#pool = pool; } // synchronous, always gets a ready pool
static async create(url) {
const pool = await connect(url);
await pool.query('SELECT 1'); // fail here, not on the first real query
return new VectorStore(pool);
}
}
const store = await VectorStore.create(process.env.DATABASE_URL);
If the object must be created synchronously and initialised lazily, cache the init promise, not the result. Chapter 5 shows why caching the result causes two connections.
Part 11: Mixing Callbacks and await
await works on promises. Callback APIs do not return promises. Mixing them goes wrong in three ways.
Awaiting a callback API. fs.readFile with a callback returns undefined. await undefined resumes on the next microtask, long before the file has been read.
import fs from 'node:fs';
let config;
await fs.readFile('./cfg.json', 'utf8', (err, data) => { config = JSON.parse(data); });
console.log(config); // undefined
An async promise executor. The Promise constructor ignores the promise that its executor returns. If the awaited call rejects, nobody calls reject. The outer promise stays pending forever, and the error becomes an unhandled rejection.
function getQuota() {
return new Promise(async (resolve) => {
const q = await fetchQuota(); // rejects
resolve(q);
});
}
// getQuota() never settles; Node reports the rejection as unhandled.
A throw inside a callback inside new Promise. The executor's own try has finished by the time the callback runs, so a throw there is an uncaught exception, not a rejection.
function readJson(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) return reject(err);
resolve(JSON.parse(data)); // throws on bad JSON: crashes the process
});
});
}
The fix for all three is the same: use the promise version of the API, and write plain async code on top of it.
import { readFile } from 'node:fs/promises';
async function readJson(path) {
return JSON.parse(await readFile(path, 'utf8')); // bad JSON is a rejection the caller can catch
}
For an old callback API with no promise version, wrap it once with util.promisify, or write the wrapper by hand following Chapter 2.
Spot the Bug
Drill 1: the nightly refund job
export async function refundFailedGenerations(batchId) {
const failed = await db.generations.findMany({
where: { batchId, status: 'failed', refundedAt: null },
});
failed.forEach(async (gen) => {
await ledger.credit(gen.userId, gen.cost, { reason: 'generation_failed', ref: gen.id });
await db.generations.update({ where: { id: gen.id }, data: { refundedAt: new Date() } });
});
await db.batches.update({ where: { id: batchId }, data: { refundsDone: true } });
logger.info({ batchId, count: failed.length }, 'refunds complete');
}
What's wrong?
Answer
Bug. forEach does not await its async callback. The batch is marked refundsDone and "refunds complete" is logged while every refund is still parked at its first await. All the refunds also start at once, with no limit. If any ledger.credit rejects, it is an unhandled rejection. Node exits, and the job runner sees a crash with the batch already marked done.
Interleaving. Callback 1 reaches await ledger.credit and yields. Callbacks 2…N do the same. forEach returns. batches.update runs and the log line prints. The credits complete later, or never, if the process dies first.
Fix.
for (const gen of failed) {
await ledger.credit(gen.userId, gen.cost, { reason: 'generation_failed', ref: gen.id });
await db.generations.update({ where: { id: gen.id }, data: { refundedAt: new Date() } });
}
await db.batches.update({ where: { id: batchId }, data: { refundsDone: true } });
Use serial code, or mapLimit if there are many. The credit and the refundedAt update also belong in one transaction, with a unique ledger reference, so that a retry cannot refund twice (Chapter 5).
Say it: "forEach throws away the promises, so the batch is marked done before any refund has run, and a failed refund crashes the process instead of failing the job."
Drill 2: provider failover
async function generate(prompt) {
try {
return primary.complete(prompt, { timeoutMs: 20_000 });
} catch (err) {
metrics.increment('llm.failover');
return fallback.complete(prompt, { timeoutMs: 20_000 });
}
}
What's wrong?
Answer
Bug. return primary.complete(...) without await. The try block exits as soon as the call returns a pending promise. When the primary times out or returns a 5xx, the rejection goes straight to the caller. The catch never runs, so failover never happens and the metric stays at zero. The dashboard says "no failovers", which looks like good news.
Fix. return await primary.complete(prompt, { timeoutMs: 20_000 });. While you are there: the primary call should get an AbortSignal, so that a timeout actually stops it (Chapter 6).
Say it: "Without await the rejection happens after we've left the try, so the catch that does the failover is dead code."
Drill 3: the usage poller
// started once at boot
setInterval(async () => {
const usage = await billingApi.listUsageEvents({ since: lastSync });
for (const e of usage) await credits.applyUsage(e);
lastSync = Date.now();
}, 5_000);
What's wrong?
Answer
Bugs.
- Overlapping runs. When the billing API call or the loop takes more than 5 s, the next tick starts with the same
lastSync, lists the same events and applies them again. Usage is double-counted. - Lost errors. One rejected
listcall is an unhandled rejection, and the process exits. - A gap.
lastSync = Date.now()is the time the loop finished, not the cutoff that was queried. Events created while the loop ran are skipped.
Fix. Use a setTimeout loop (startPolling from Part 8) so that runs never overlap. Catch inside the tick. Move the cursor to the newest event actually applied. Make applyUsage idempotent by event ID with a unique constraint, because with two instances the timer runs in both.
Say it: "setInterval doesn't wait for an async callback, so slow runs overlap and apply the same events twice, and one failure kills the process."
Drill 4: dashboard loader
export async function loadAdminDashboard(orgId) {
const membersP = db.members.count({ where: { orgId } });
const spendP = billing.monthToDate(orgId); // calls an external API
const jobsP = db.jobs.groupBy({ by: ['status'], where: { orgId } });
try {
return {
members: await membersP,
spend: await spendP,
jobs: await jobsP,
};
} catch (err) {
logger.warn({ err }, 'dashboard partial failure');
return { members: null, spend: null, jobs: null };
}
}
What's wrong?
Answer
Bug. "Start early, await later." If billing.monthToDate rejects while the function is still waiting on membersP, spendP has no handler at that moment. Node raises unhandledRejection and exits with code 1. The try/catch never runs. The test suite passes, because in tests the member count is fast and resolves first.
Fix.
const [members, spend, jobs] = await Promise.allSettled([
db.members.count({ where: { orgId } }),
billing.monthToDate(orgId),
db.jobs.groupBy({ by: ['status'], where: { orgId } }),
]);
return {
members: members.status === 'fulfilled' ? members.value : null,
spend: spend.status === 'fulfilled' ? spend.value : null,
jobs: jobs.status === 'fulfilled' ? jobs.value : null,
};
allSettled attaches handlers to all three at once and also gives a real partial result, which is what the catch was trying to do.
Say it: "A promise has to have a handler by the time it rejects; awaiting it later is too late if it rejects while we're parked on an earlier await."
The Causal Chains
- If you pass an async callback to
forEach - If you
filterwith an async predicate - If you write
return pinside atry - If an async function is called without
awaitinsidetry - If you start two promises and await them one at a time
- If you
setIntervalan async job slower than the interval - If
Promise.allrejects on the first failure - If a module uses top-level await on a network call
Review Checklist
grep -n "forEach(async"and.filter(async,.some(async,.every(async,.sort(async: almost always bugs..map(asyncwhose result is not passed toPromise.all,allSettledor a pool.return <call>insidetryblocks in async functions: should it bereturn await?- Calls to async functions on their own line with no
await,return,void … .catch: floating promises. Turn onno-floating-promises. - Promises created and stored in variables, then awaited later one by one.
Promise.all(over anything whose length comes from a request, a query result or a queue.Promise.allaround side-effecting calls: what happens to the others when one fails?setInterval(asyncand async listeners onEventEmitter, Express 4 routes, stream'data'events.new Promise(async, andawaiton anything that takes a callback.- Top-level
awaitoutside the entry module; constructors that start async work.
What's Next
Every trap here comes back to Part 1: an await is a point where other code runs. Chapter 5 — Race Conditions in Single-Threaded JS asks what happens when that other code touches the same state.