JavaScript Part 2
Promises and async/await From the Ground Up
Promise states, microtask order, a promise built by hand, async/await, the combinators and AbortController.
On this page
- Words used in this lesson
- Part 1. The problem: callbacks
- Part 2. A promise is a one-time value with three states
- Part 3. then, catch, finally: the chaining rules
- Part 4. Microtasks: why the order is what it is
- Part 5. Build a tiny Promise by hand
- Part 6. async/await is sugar
- Part 7. The combinators
- Part 8. Errors and unhandled rejections
- Part 9. Cancellation with AbortController
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: a promise is an object that stands for one value that will arrive later (or an error). It starts as pending, settles once, and lets you attach code with then. async/await is just a nicer way to write the same promise chains. Almost every "order of output" puzzle is solved by one rule: promise callbacks run in the microtask queue, after the current code and before the next timer.
This lesson builds promises from the problem they solve, then builds a small one by hand and runs it. It assumes you know the call stack and the event loop, and that functions can be passed as values (first-class functions). For interview question banks see Async JavaScript, Promises and the Event Loop and JavaScript Core Interview Questions. For streams and blocking the loop in Node see Node.js Performance Basics.
What I ran / checked. Everything was run with Node v22.22.0 on Linux (CommonJS files unless I say .mjs). Output is pasted as printed, except that I cut Node's long stack traces. Millisecond timings are from this machine, rounded, and only show the shape (sequential against parallel); they are not benchmarks. For unhandled rejections I read the --unhandled-rejections section of doc/api/cli.md in the nodejs/node repository and ran the cases. I did not run anything in a browser, and browsers differ on the unhandled-rejection part.
Words used in this lesson
- Asynchronous means "the result comes later, and the program does not wait". The code after the call runs first.
- Callback is a function you hand to someone else to call later.
- Promise is an object for a value that is not ready yet.
- Settled means a promise is no longer pending. It is either fulfilled (has a value) or rejected (has a reason, usually an
Error). - Microtask is a small job in a queue that the engine runs right after the current code finishes, before timers and I/O.
- Thenable is any object with a
thenmethod. Executor is the function you give tonew Promise(...). - Unhandled rejection is a rejected promise that has no handler by the time the engine checks.
Part 1. The problem: callbacks
Reading a file or calling a server takes time. JavaScript has one thread (see the engine lesson), so it cannot wait. The old answer was a callback: "call this function when you are done". In Node the old style is error-first: the callback gets (err, data).
When one step needs the result of the previous one, the callbacks nest:
const delay = (ms, cb) => setTimeout(cb, ms);delay(5, () => { console.log("step 1"); delay(5, () => { console.log("step 2"); delay(5, () => { console.log("step 3 (pyramid)"); }); });});const sleep = (ms) => new Promise((r) => setTimeout(r, ms));sleep(30).then(() => { console.log("p step 1"); return sleep(5); }).then(() => { console.log("p step 2"); return sleep(5); }).then(() => console.log("p step 3 (flat)"));Output:
step 1step 2step 3 (pyramid)p step 1p step 2p step 3 (flat)The pyramid is "callback hell". Its real problems are not the shape:
- Error handling is manual. Every callback needs its own
if (err). Atry/catcharound the outer call cannot catch an error thrown later in an inner callback. - Inversion of control. You give your code to a library and trust it to call you once, not twice, not never.
- No value you can hold. You cannot return the "result so far" or pass it around.
A promise fixes these: it is a value you can pass around, it settles once, and errors flow down a chain.
Part 2. A promise is a one-time value with three states
const p = new Promise((resolve, reject) => { console.log("executor runs now"); resolve(1); resolve(2); reject(new Error("late")); });console.log("after constructor", p);p.then((v) => console.log("then got", v));const pending = new Promise(() => {});const rejected = Promise.reject("nope"); rejected.catch(() => {});console.log(pending, rejected);console.log(Promise.resolve(5));Output:
executor runs nowafter constructor Promise { 1 }Promise { <pending> } Promise { <rejected> 'nope' }Promise { 5 }then got 1 resolve(value) state = fulfilled, has a valuepending ----------------------------> | reject(reason) state = rejected, has a reason +---------------------------------->Rules this shows:
- The executor runs immediately and synchronously. "executor runs now" came first.
- A promise settles once. The second
resolve(2)and therejectwere ignored. The value stays1. - Once settled, a promise never changes. Anyone who calls
.thenlater still gets the same value. thencallbacks never run synchronously, even for a promise that is already settled. "then got 1" printed last, after all the synchronous lines. This keeps the order predictable.Promise.resolve(v)andPromise.reject(r)make an already-settled promise.
Part 3. then, catch, finally: the chaining rules
p.then(onFulfilled, onRejected) returns a new promise. That is why you can chain. What the new promise becomes depends on what your callback does:
| Your callback... | The new promise... |
|---|---|
returns a normal value x | fulfills with x |
| returns a promise or thenable | follows it (waits, then copies its result) |
| throws | rejects with the thrown error |
| is missing (not a function) | passes the value or error through unchanged |
Promise.resolve(1) .then((v) => { console.log("A", v); return v + 1; }) .then((v) => { console.log("B", v); return new Promise((r) => setTimeout(() => r(v * 10), 10)); }) .then((v) => { console.log("C", v); throw new Error("oops"); }) .then(() => console.log("skipped")) .catch((e) => { console.log("caught", e.message); return "recovered"; }) .then((v) => console.log("D", v)) .finally(() => { console.log("finally"); return "ignored"; }) .then((v) => console.log("E", v));const same = Promise.resolve(1);console.log(same.then(() => {}) === same);Promise.resolve(1).then(2).then((v) => console.log("non-function ignored:", v));Promise.resolve().then(() => { throw 1; }).finally(() => console.log("fin2")).catch((x) => console.log("still rejected with", x));Output:
falseA 1B 2non-function ignored: 1fin2still rejected with 1C 20caught oopsD recoveredfinallyE undefined(The two small chains and the long chain interleave. Follow each by its own letters.) Read the long chain:
Areturns2, so the nextthengets2(B 2).Breturns a promise that resolves after 10 ms, so the chain waits and then continues with its value20(C 20).Cthrows. The nextthen(() => ...)has no rejection handler, so it is skipped: the error passes through."skipped"was never printed..catchreceives the error. Returning"recovered"makes the chain fulfilled again, soD recovered..finally(fn)runsfnwith no arguments, whatever happened. It ignores the return value (E undefined: the value fromDwasundefined, and"ignored"did not replace it). Iffnthrows, the chain rejects. Thefin2chain shows thatfinallydoes not stop a rejection (still rejected with 1).thenreturns a new promise each time (false), and a non-function argument (then(2)) is ignored, so the value1passed through.
p.then(a, b) is not the same as p.then(a).catch(b). In the first form, b handles errors from p only, not errors thrown by a. I ran it: an error thrown in a was caught by the following .catch, and b never ran. Usually you want .then(a).catch(b).
Part 4. Microtasks: why the order is what it is
Your callbacks do not run at once. When a promise settles (or is already settled), the engine puts the callback in the microtask queue. The event loop (see Async JS and the Event Loop) does this:
- Run the current script or callback to completion.
- Run all microtasks, including new ones added while it runs, until the queue is empty.
- Only then take the next task (a timer, an I/O callback).
So microtasks always beat timers, even setTimeout(fn, 0). A traced example:
console.log("1 sync");setTimeout(() => console.log("2 timeout"), 0);Promise.resolve().then(() => console.log("3 promise 1")).then(() => console.log("4 promise 2"));queueMicrotask(() => console.log("5 microtask"));process.nextTick(() => console.log("6 nextTick"));(async () => { console.log("7 async start"); await null; console.log("8 after await"); })();console.log("9 sync end");Output (CommonJS):
1 sync7 async start9 sync end6 nextTick3 promise 15 microtask8 after await4 promise 22 timeoutTrace:
- Sync lines run first:
1, then7(an async function runs synchronously until its firstawait), then9. - Registered along the way: timer (task queue);
thenfor3(microtask queue, because the promise is already resolved);queueMicrotaskfor5; theawait nullcontinuation for8; andnextTickfor6. process.nextTickis a Node-only queue. It runs before promise microtasks when the main script ends in CommonJS, so6comes first.- Then the microtasks run in the order they were added:
3,5,8. Running3added4to the end of the queue, so4came after8. - The timer runs last:
2.
I also ran the same file as an ES module (.mjs). Only one thing changed: 6 nextTick moved to after 4 promise 2, just before 2 timeout.
In an ES module the whole file runs inside a promise job, so we are already in the microtask phase when it ends, and nextTick waits until the queue is empty. Do not rely on mixing nextTick and promises. The browser has no nextTick.
Resolving with a promise costs extra rounds
When you resolve a promise with another promise (or return one from then), the engine needs extra microtask rounds to unwrap it. In a test I ran, a chain Promise.resolve().then(a1).then(a2).then(a3) and new Promise(r => r(Promise.resolve())).then(b1) printed b1 only after a3, two rounds later than a plain then would. Do not write code that depends on exact round counts.
Part 5. Build a tiny Promise by hand
Building one removes the magic. The rules we need: a state, a value, a list of waiting handlers; settle once; handlers run in a microtask; then returns a new promise; a returned thenable is followed.
class MyPromise { #state = "pending"; #value; #handlers = []; constructor(executor) { const resolve = (v) => this.#settle("fulfilled", v); const reject = (e) => this.#settle("rejected", e); try { executor(resolve, reject); } catch (e) { reject(e); } } #settle(state, value) { if (this.#state !== "pending") return; // settle once if (state === "fulfilled" && value && typeof value.then === "function") { let called = false; try { value.then((v) => { if (!called) { called = true; this.#settle("fulfilled", v); } }, (e) => { if (!called) { called = true; this.#settle("rejected", e); } }); } catch (e) { if (!called) this.#settle("rejected", e); } return; } this.#state = state; this.#value = value; this.#handlers.forEach((h) => this.#run(h)); this.#handlers = []; } #run({ onFulfilled, onRejected, resolve, reject }) { queueMicrotask(() => { // handlers always run async const cb = this.#state === "fulfilled" ? onFulfilled : onRejected; if (typeof cb !== "function") { // pass through return this.#state === "fulfilled" ? resolve(this.#value) : reject(this.#value); } try { resolve(cb(this.#value)); } catch (e) { reject(e); } }); } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { const h = { onFulfilled, onRejected, resolve, reject }; if (this.#state === "pending") this.#handlers.push(h); else this.#run(h); }); } catch(onRejected) { return this.then(undefined, onRejected); }}console.log("start");new MyPromise((res) => setTimeout(() => res(20), 10)) .then((v) => { console.log("got", v); return v + 1; }) .then((v) => { console.log("got", v); throw new Error("fail"); }) .then(() => console.log("skipped")) .catch((e) => { console.log("caught", e.message); return "ok"; }) .then((v) => console.log("end", v));new MyPromise((res) => res(1)).then((v) => console.log("sync-resolved, still async:", v));console.log("sync end");Output:
startsync endsync-resolved, still async: 1got 20got 21caught failend okHow it works, in five lines:
#settlesets the state once and then runs every waiting handler. If the value is a thenable, it follows it by calling itstheninstead of settling now.#runwraps each handler inqueueMicrotask. That is the whole "never synchronous" rule.thenreturns a newMyPromise. Itsresolveis called with whatever your callback returns, and itsrejectwith whatever it throws. That is the table from Part 3.- If your callback is not a function,
#runpasses the value or error on. That is the pass-through rule. catch(f)is onlythen(undefined, f).
It lacks things the real one has (a rule against resolving a promise with itself, exact tick counts), so do not use it in production. But all the chaining behaviour from Part 3 comes out of these few lines.
Part 6. async/await is sugar
An async function always returns a promise. await x pauses that function (not the thread), waits for the promise x, and resumes it in a microtask with the value, or throws the rejection reason at that line.
const sleep = (ms, v, fail) => new Promise((res, rej) => setTimeout(() => (fail ? rej(new Error(v)) : res(v)), ms));async function main() { const r = async () => { throw new Error("from async"); }; try { await r(); } catch (e) { console.log("caught:", e.message); } const t0 = Date.now(); await sleep(50, "a"); await sleep(50, "b"); const seq = Date.now() - t0; const t1 = Date.now(); const both = await Promise.all([sleep(50, "a"), sleep(50, "b")]); const par = Date.now() - t1; console.log(both, "sequential ~" + Math.round(seq / 10) * 10 + "ms, parallel ~" + Math.round(par / 10) * 10 + "ms"); const log = []; [1, 2, 3].forEach(async (n) => { await sleep(5); log.push(n); }); console.log("after forEach:", log); await sleep(30); console.log("later:", log); for (const n of [1, 2]) { await sleep(1); log.push("for" + n); } console.log(log); async function a() { try { return sleep(1, "x", true); } catch { return "never"; } } async function b() { try { return await sleep(1, "x", true); } catch { return "handled"; } } console.log(await a().catch((e) => "a rejected: " + e.message), await b());}main();Output:
caught: from async[ 'a', 'b' ] sequential ~100ms, parallel ~50msafter forEach: []later: [ 1, 2, 3 ][ 1, 2, 3, 'for1', 'for2' ]a rejected: x handledWhat it shows:
try/catchworks aroundawait. This is the biggest gain over callbacks: one error style for sync and async code.- Sequential against parallel. Two awaits one after the other took about 100 ms. Starting both first and awaiting
Promise.alltook about 50 ms. Await only when you need the result. (These timings are from this machine and are rounded.) forEachdoes not wait. It ignores the promises its callback returns, so "after forEach" saw an empty list. Usefor...ofwithawaitfor one-by-one, orawait Promise.all(items.map(async ...))for parallel.returnagainstreturn await. Insidetry,return promiseleaves the function before the rejection happens, so thecatchnever ran (a rejected: x). Withreturn await, thecatchran (handled).- The code before the first
awaitruns synchronously; everything after it runs as a microtask. That is why7 async startprinted in the sync part in Part 4.
Part 7. The combinators
| Function | Fulfills when | Rejects when | Result |
|---|---|---|---|
Promise.all(list) | all fulfill | any rejects (first one) | array of values, in input order |
Promise.allSettled(list) | all settle | never | array of {status, value} or {status, reason} |
Promise.race(list) | the first settles with a value | the first settles with an error | that first result |
Promise.any(list) | the first fulfills | all reject: AggregateError | the first value |
const sleep = (ms, v, fail) => new Promise((res, rej) => setTimeout(() => (fail ? rej(new Error(v)) : res(v)), ms));(async () => { const show = (label, p) => p.then((v) => console.log(label, "->", v), (e) => console.log(label, "-> rejected:", e.constructor.name, e.message)); await show("all ok", Promise.all([sleep(10, 1), sleep(5, 2), 3])); await show("all fail", Promise.all([sleep(10, 1), sleep(5, "bad", true)])); await show("allSettled", Promise.allSettled([sleep(5, 1), sleep(5, "bad", true)])); await show("race", Promise.race([sleep(20, "slow"), sleep(5, "fast")])); await show("race reject", Promise.race([sleep(20, "slow"), sleep(5, "err", true)])); await show("any", Promise.any([sleep(5, "e1", true), sleep(10, "winner"), sleep(1, "e2", true)])); await show("any none", Promise.any([sleep(5, "e1", true), sleep(1, "e2", true)])); await show("all empty", Promise.all([])); await show("any empty", Promise.any([]));})();Output (I removed the stack trace that Node printed inside the allSettled reason):
all ok -> [ 1, 2, 3 ]all fail -> rejected: Error badallSettled -> [ { status: 'fulfilled', value: 1 }, { status: 'rejected', reason: Error: bad ... }]race -> fastrace reject -> rejected: Error errany -> winnerany none -> rejected: AggregateError All promises were rejectedall empty -> []any empty -> rejected: AggregateError All promises were rejectedall: results keep input order ([1, 2, 3]), not finish order. Plain values are allowed.allfails fast. The other promises are not cancelled; they keep running and their results are ignored.allSettledis for "try all, report each". Use it for batch jobs.racetakes the first settled, good or bad. A classic use is a timeout.anyignores errors until one succeeds. Use it for "try mirrors, take the first answer".- Edge cases:
all([])fulfills with[], butany([])rejects.
Limiting parallelism
Promise.all(items.map(fetchOne)) starts everything at once. For 10,000 items, that can overload a server. A small pool has N workers that each pull the next task in a loop (while (i < tasks.length) { const k = i++; res[k] = await tasks[k](); }, started N times inside Promise.all). I ran it with four 20 ms tasks and a limit of 2: it returned [ 1, 2, 3, 4 ] in about 40 ms (two rounds of two). With Promise.all all four would start together. Each worker is an async loop; i++ is safe because JavaScript runs one piece of code at a time.
Part 8. Errors and unhandled rejections
An error thrown in an executor, in a then callback, or in an async function becomes a rejection (I ran all three; each reached a .catch). Errors thrown in a plain setTimeout callback are not caught by a promise; they need their own try/catch. Forgetting await is a common bug: you get a promise object (Promise { 'data' } in my run) where you wanted data.
What Node does with an unhandled rejection
After a rejection happens with no handler attached, Node waits for the microtask queue to drain. If there is still no handler, it follows the --unhandled-rejections mode. The default (since Node 15) is throw: emit an unhandledRejection event; if nobody listens, raise it as an uncaught exception, which ends the process. I ran:
const p = Promise.reject(new Error("nobody handles me"));setTimeout(() => console.log("timer still ran?"), 50);The process printed the error with a stack trace, ended with the line Node.js v22.22.0, never printed "timer still ran?", and the exit code was 1. With node --unhandled-rejections=warn it printed an UnhandledPromiseRejectionWarning instead and went on.
You can listen for the events. This also shows the late-handler case:
process.on("unhandledRejection", (reason, promise) => { console.log("unhandledRejection:", reason.message); });process.on("rejectionHandled", () => console.log("rejectionHandled (handler added late)"));const p = Promise.reject(new Error("late handler"));setTimeout(() => p.catch(() => console.log("handled now")), 20);Output:
unhandledRejection: late handlerhandled nowrejectionHandled (handler added late)The promise was reported as unhandled because the catch came 20 ms later. Attach handlers in the same tick as you create the promise. In production, log in an unhandledRejection handler and then let the process restart; do not just hide it. (Browsers fire a similar unhandledrejection event on window; I did not run that.)
Part 9. Cancellation with AbortController
A promise cannot be cancelled. What you can do is make the work stop and reject. The standard tool is AbortController: it gives you a signal to pass into the work, and a method abort() to trigger it.
const sleep = (ms, signal) => new Promise((res, rej) => { signal?.throwIfAborted(); const t = setTimeout(res, ms, "done"); signal?.addEventListener("abort", () => { clearTimeout(t); rej(signal.reason); }, { once: true });});(async () => { const ac = new AbortController(); setTimeout(() => ac.abort(), 20); try { await sleep(1000, ac.signal); } catch (e) { console.log("1:", e.name, "|", e.message, "|", ac.signal.aborted); } try { await sleep(1000, AbortSignal.timeout(20)); } catch (e) { console.log("2:", e.name, "|", e.message); } const ac2 = new AbortController(); ac2.abort(new Error("custom reason")); try { await sleep(10, ac2.signal); } catch (e) { console.log("3:", e.message); } const server = require("http").createServer((req, res) => setTimeout(() => res.end("slow"), 500)).listen(0); const url = `http://127.0.0.1:${server.address().port}`; try { await fetch(url, { signal: AbortSignal.timeout(50) }); } catch (e) { console.log("4 fetch:", e.name, "|", e.message); } try { const ac4 = new AbortController(); setTimeout(() => ac4.abort(), 50); await fetch(url, { signal: ac4.signal }); } catch (e) { console.log("5 fetch:", e.name, "|", e.message); } server.closeAllConnections(); server.close();})();Output:
1: AbortError | This operation was aborted | true2: TimeoutError | The operation was aborted due to timeout3: custom reason4 fetch: TimeoutError | The operation was aborted due to timeout5 fetch: AbortError | This operation was aborted- A signal fires an
abortevent once. If it is already aborted when you start, the event will not fire again, so checksignal.abortedor callsignal.throwIfAborted()first (case 3). My first version without that check did not reject: it waited 10 ms and returned"done". AbortSignal.timeout(ms)is a ready-made signal that aborts with aTimeoutError(cases 2 and 4). A normalabort()gives anAbortError(cases 1 and 5). So you can tell "user cancelled" from "too slow".fetchaccepts{ signal }. The request is cancelled and the promise rejects.- Typical use: in a React effect or a search box, abort the previous request when a new one starts, so an old slow answer cannot overwrite a newer one.
Try it
- Predict the order:
console.log("a"); setTimeout(() => console.log("b")); Promise.resolve().then(() => console.log("c")); console.log("d"); - What does
Promise.resolve(1).then(2).then(console.log)print? async function f() { try { return Promise.reject(new Error("x")); } catch { return "caught"; } }What doesawait f()do?- How would you add a 2-second timeout to any promise-returning function, using only what is in this lesson?
- You have an array of 5 user IDs. Write code that loads them in parallel, keeps going if some fail, and prints how many succeeded.
Answers:
a d c b. Sync first (a,d), then the microtask (c), then the timer (b). I ran it and got that order.- It prints
1(I ran it). A non-function argument tothenis ignored, so the value passes through (shown in Part 3). - It throws (rejects) with
x;"caught"is never returned. Withoutawaiton the returned promise, thetryblock is already left. Usereturn await. I ran it: the promise rejected withx. Promise.race([work(), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 2000))]), I ran theraceversion with 20 ms against a 100 ms job and gottimeout. Better isAbortSignal.timeout(2000)passed to the work, becauseracedoes not stop the slow work. Also clear the timer if the work wins.const rs = await Promise.allSettled(ids.map(loadUser)); const ok = rs.filter(r => r.status === "fulfilled").length; console.log(ok + " of " + rs.length);I ran it with aloadUserthat fails for odd IDs and it printed2 of 5.
Interview questions
What is a promise and what are its states? (Must know)
A promise is an object that represents a value that will be available later. It is pending at first. It then becomes fulfilled with a value or rejected with a reason, exactly once, and never changes after that. Handlers added with then, catch or finally run asynchronously, in the microtask queue.
What does then return, and how do chains work? (Must know)
then always returns a new promise. If my callback returns a value, the new promise fulfills with it. If I return a promise, the new one follows it. If I throw, it rejects. If I pass no handler for a case, the result passes through. So an error skips then steps until a catch, and a catch that returns normally turns the chain back to fulfilled.
Give the output order of sync code, setTimeout(0), a promise then and process.nextTick. (Must know)
Sync code first. Then in Node CommonJS: nextTick queue, then promise microtasks, then timers. In the browser there is no nextTick: sync, microtasks, then timers. In an ES module in Node, promise microtasks can run before nextTick, because the module itself runs inside a promise job. I would say the safe answer: microtasks always run before the next timer.
How would you implement a simple Promise? (Senior signal)
Keep a state, a value and a list of handlers. resolve and reject change the state only if still pending, then run the handlers. then returns a new promise and wraps each handler in queueMicrotask. It resolves the new promise with the callback's return value (following it if it is a thenable) or rejects it if the callback throws. If the callback is not a function, it passes the value on.
What is the difference between Promise.all, allSettled, race and any? (Must know)
all waits for all successes and fails on the first failure. allSettled waits for all and reports each outcome. race settles with the first result, good or bad. any fulfills with the first success and rejects with an AggregateError only if all fail. None of them cancel the other promises.
Sequential or parallel: what is the cost of a careless await? (Must know)
Two independent calls awaited one after the other take the sum of both times. Starting both first and then awaiting them together takes the longer one. In my run it was about 100 ms against 50 ms. I await only when the next line needs the result. I limit parallelism with a pool when the number of items is large.
What happens with an unhandled rejection in Node? (Senior signal)
Since Node 15 the default mode is throw: an unhandledRejection event is emitted, and if no listener exists the error becomes an uncaught exception and the process exits with code 1. In my test a timer scheduled after the rejection never ran. I always attach a catch, and I add a global handler that logs and lets the process restart.
A promise cannot be cancelled. How do you stop work that is not needed any more? (Senior signal)
I use AbortController. I pass its signal to fetch or to my own function. On abort() the work stops and the promise rejects with the signal's reason. AbortSignal.timeout(ms) gives a timeout with a TimeoutError. I must handle an already-aborted signal with throwIfAborted(), because the event fires only once. In a UI, I abort the previous request when a new one starts.
Key takeaways
- A promise is a one-time value: pending, then fulfilled or rejected, once. Handlers always run asynchronously as microtasks, before the next timer.
thenreturns a new promise: a returned value fulfills it, a returned promise is followed, a throw rejects it, and a missing handler passes the result through.async/awaitis the same promises withtry/catch. Code before the firstawaitruns synchronously.- Await only when you need the value. Use
Promise.allfor independent work,allSettledwhen you want every outcome, and a pool for large lists. forEachdoes not wait.return awaitis needed insidetry/catch.- Node ends the process on an unhandled rejection by default. Handle errors close to where you create the promise.
- Cancel work with
AbortControllerandAbortSignal.timeout, and checksignal.abortedbefore you start.
What's next
Next: Modules: ES Modules, CommonJS, Bundlers and Tree Shaking, Lesson 3. Practise: write a retry(fn, times) that uses await in a loop, then add an AbortSignal so a caller can stop the retries.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series