- Learn
- Computer Science & IT
- Programming
- JavaScript Programming
- Asynchronous JavaScript
Asynchronous JavaScript
await. Handle failures with catch, try and finally, and run independent tasks at the same time with Promise.all and the other promise combinators.IntermediateLesson 7 of 9 in JavaScript Programming Course
What you will learn
- Tell synchronous code from asynchronous code, follow the event loop through the call stack and its queues, and schedule work with timers.
- Tell synchronous code from asynchronous codeExplain why JavaScript runs one piece of your code at a time, why long-running code freezes a page, and how asynchronous operations let a program keep working while it waits.
- Follow the event loopTrace how the call stack, the task queue and the microtask queue decide the order in which code runs, and predict the output of code that mixes timers and promises.
- Schedule code with timersRun code once after a delay or repeatedly with setTimeout and setInterval, cancel timers, and explain why a timer's delay is a minimum wait rather than an exact time.
- Tell synchronous code from asynchronous code
- Pass functions to asynchronous operations, report errors with the error-first convention, and recognize why deeply nested callbacks are hard to read and maintain.
- Pass callbacks to asynchronous operationsWrite functions that accept a callback, tell synchronous callbacks from asynchronous ones, and get an asynchronous result to the code that needs it.
- Handle errors in callbacks and avoid callback hellUse the error-first callback convention, explain why try and catch cannot catch an error thrown later in a callback, and flatten deeply nested callback code.
- Pass callbacks to asynchronous operations
- Create promises, react to their results with then, catch and finally, and chain asynchronous steps into a readable sequence.
- Explain promise states and create a promiseDescribe the states a promise can be in, create promises with the Promise constructor, and wrap a timer in a promise.
- React to promises with then, catch and finallyRegister handlers that run when a promise is fulfilled, rejected or settled, and use them to update a page at the right moment.
- Chain promises into a sequenceReturn values and promises from handlers to build flat chains of dependent steps, and avoid the missing-return and nesting mistakes.
- Explain promise states and create a promise
- Write async functions, pause them with await until promises settle, rewrite promise chains and callback APIs in this style, and avoid the common mistakes of async code.
- Write async functions and use awaitDeclare async functions, explain what they return, and use await to pause a function until a promise settles without blocking the page.
- Rewrite promise chains and callback APIs with async and awaitTurn promise chains into async functions, wrap callback-based functions in promises so they can be awaited, and choose a clear style.
- Avoid common async and await mistakesRecognize and fix forgotten awaits, async callbacks in forEach, promises used as conditions and async calls whose errors go unhandled.
- Write async functions and use await
- Catch and recover from failures in promise chains and with try and catch, clean up with finally, and make sure failures never go unnoticed.
- Handle rejections in promise chainsFollow how a rejection travels down a promise chain, recover from it or pass it on, and place catch handlers where they handle the right failures.
- Use try, catch and finally with awaitUse Error objects and the try, catch and finally statement to handle failures of awaited promises, clean up reliably and give useful fallbacks.
- Keep failures visible and recover gracefullyPrevent unhandled rejections, avoid swallowing errors, add context to errors, and choose between retrying, using a fallback and informing the user.
- Handle rejections in promise chains
- Start independent tasks at the same time, wait for them together, and choose the promise combinator that fits all-or-nothing, best-effort, first-to-finish and first-success situations.
- Start tasks together and wait with Promise.allRun independent asynchronous tasks at the same time, combine their results with Promise.all, and explain its timing and failure behavior.
- Choose between Promise.allSettled, Promise.race and Promise.anyUse allSettled when every outcome matters, race for the first task to settle, such as a timeout, and any for the first success, and compare all four combinators.
- Start tasks together and wait with Promise.all
Asynchronous JavaScript study guide
1Explain how JavaScript runs asynchronous code
JavaScript often needs to wait while a person clicks or a timer counts down. During that wait, the page should keep responding. Build a picture of what runs now, what waits in the environment, and what joins a queue for later. Then use that picture to trace console output and schedule timers. The key is to track when a function is called, not just where its name appears in the code.
1.1 Tell synchronous code from asynchronous code
Synchronous code finishes the current statement before moving to the next. Your page’s JavaScript runs on one main thread, so two pieces of that code do not execute at the same instant. This is easy to follow until a statement starts work whose result will arrive later.
Start now, handle the result later
An asynchronous operation starts now and delivers its result afterward, through a callback or a promise. The browser or Node.js is the host environment. It manages the waiting while your program continues. Timers, user input, and image loading all use this kind of arrangement.
console.log("Opening");
setTimeout(() => console.log("Ready"), 700);
console.log("Waiting");
Read the call to setTimeout as “register this function.” It returns without waiting 700 ms. The current script therefore logs "Opening" and "Waiting" first. After about 700 ms, the callback can run and log "Ready". The exact delay also depends on when the main thread becomes free.
There are two different activities here: the environment tracks the timer, and JavaScript eventually calls the function that logs the result. The waiting overlaps other work; the callback still runs on the main thread.
Why a page freezes
A long synchronous loop keeps the thread occupied. While it runs, click handlers and timer callbacks cannot execute, and the page cannot update its display through that thread. Registering a zero-delay timer does not give it permission to interrupt the loop.
Suppose a timer becomes ready while a calculation is still running. The environment queues its callback. Finishing the calculation is what frees the thread; the timer’s deadline alone cannot do that. A callback that performs another long calculation can freeze the page too.
To reduce a calculation’s effect on responsiveness, divide it into smaller pieces that allow other tasks between them, or move suitable work to a web worker, a separate background thread provided by the browser. Simply putting a large loop inside an async function leaves that loop on the main thread.
Three ways to receive a result
A callback passes the next action as a function. A promise represents one eventual outcome and lets you attach success and failure handlers. An async function works with promises while await lets its own execution pause at a clear point.
These styles coexist. A timer still takes a callback even when the surrounding code uses promises. An async function can await a promise produced by a callback wrapper. Choose the style that makes the result’s path easy to follow.
When reading asynchronous code, first mark the statements that run during the current call. Next mark the functions registered for later. A line inside a registered callback does not run merely because execution has reached the line that creates that function.
Key Takeaways
- Synchronous JavaScript occupies the main thread until the current work finishes.
- The host environment manages waiting and schedules result-handling code.
- Asynchronous callbacks run later on the main thread.
- An async function can still block the page with a long calculation.
1.2 Follow the event loop
The event loop coordinates work that is ready to run. To predict output, keep three things separate: the functions running now, callbacks waiting as tasks, and promise-related work waiting as microtasks.
Follow the stack and queues
The call stack holds active function calls. Calling a function pushes a frame onto it; returning removes that frame. The current function runs until it returns or calls another function.
A task can be a script, a timer callback, or a click handler. Tasks run one at a time. Browsers can have multiple task queues, so do not assume every source of work shares one universal first-in, first-out queue.
The microtask queue contains promise handlers from then, catch, and finally, continuations after await, and callbacks passed to queueMicrotask. At a microtask checkpoint after the current work, the queue drains before the next task. Microtasks added during that drain also run before the next task.
Trace one script
console.log("begin");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("finish");
- The script logs "begin".
- It registers a timer callback; it does not call it.
- It queues the fulfilled promise’s handler as a microtask.
- It logs "finish" and finishes the current work.
- The microtask logs "promise".
- The timer task logs "timer".
An already fulfilled promise still schedules its handler for later. A delay of 0 ms still schedules a timer task.
Include jobs added along the way
Promise.resolve()
.then(() => console.log("first"))
.then(() => console.log("third"));
queueMicrotask(() => console.log("second"));
console.log("now");
The script logs "now". The microtask queue initially holds the first promise handler, then the queueMicrotask callback. When the first handler logs "first" and returns, it makes the next promise handler ready. That new handler joins the end of the queue. The remaining output is "second", then "third".
Completion and responsiveness
Once a task or microtask begins, asynchronously queued work cannot interrupt it. This run-to-completion behavior lets you reason about a local change without a timer suddenly entering halfway through the function.
After microtasks finish, the browser can update the screen at a rendering opportunity. It does not have to repaint after every task. An endless stream of microtasks can prevent both later tasks and rendering: each new microtask keeps the current drain going. Deferring work to a microtask therefore does not guarantee that the page gets a chance to respond.
For a trace, write the immediate output first, drain microtasks in order, and only then move to ready timer tasks. Add newly scheduled work to the appropriate queue when the code actually schedules it.
Track a timer created by a microtask
setTimeout(() => console.log("outer timer"), 0);
Promise.resolve().then(() => {
console.log("job one");
setTimeout(() => console.log("inner timer"), 0);
}).then(() => console.log("job two"));
console.log("script");
The script registers the outer timer and the first promise handler, then logs "script". The first microtask logs "job one" and registers the inner timer. Returning from that handler queues "job two", which also runs before a timer task. The output ends with "outer timer", then "inner timer": both timers have the same delay, and the outer one was registered first.
Creating a timer during a microtask does not make its callback a microtask. Classify the work by the scheduling API that receives it, then add it to the appropriate queue.
Key Takeaways
- The call stack records active calls.
- Promise handlers and await continuations run as microtasks.
- Drain newly added microtasks before taking another task.
- Queued callbacks cannot interrupt currently running JavaScript.
- Rendering is possible after microtasks, not guaranteed after every task.
1.3 Schedule code with timers
Timers schedule callbacks for later. They measure a delay in milliseconds: 1000 ms is 1 s. The delay sets when a callback can become eligible to run; other work and browser limits can make it run later.
Schedule and cancel one call
function remind(topic) {
console.log("Review " + topic);
}
const reminder = setTimeout(remind, 2000, "promises");
// To cancel before it runs:
clearTimeout(reminder);
Extra arguments after the delay are passed to the callback. A missing delay counts as 0. Save the returned timer handle if you may need to cancel it. Browsers return a numeric ID; Node.js returns a Timeout object. Both work with clearTimeout. Clearing a finished or nonexistent timer has no effect.
Pass a function: setTimeout(remind, 2000, "promises"). Writing setTimeout(remind("promises"), 2000) calls remind immediately and passes its return value to the timer. An arrow function is another way to delay a call with arguments: setTimeout(() => remind("promises"), 2000).
Repeat with an interval or another timeout
let ticks = 0;
const clock = setInterval(() => {
ticks++;
console.log("Tick " + ticks);
if (ticks === 3) clearInterval(clock);
}, 1000);
The first tick is after about one second. The third callback stops future ticks. Keep the interval handle and use clearInterval when the repeating work should end.
A callback can instead schedule its own next timeout. When it calls setTimeout at the end of its synchronous work, the next delay starts after that work. This gives a pause between runs and allows a different delay each time. An interval keeps asking for runs on a fixed schedule; slow work can leave little pause. If a run starts asynchronous work, wait for that work too before scheduling the next run when overlap would be a problem.
Timing has limits
A busy thread delays ready callbacks. Browsers also throttle timers in background tabs, often to about one check per second or less. When the timer nesting level is greater than five, the HTML timer rule raises delays below 4 ms to 4 ms. Very large delays also need care: the usual signed 32-bit limit is 2147483647 ms, about 24.8 days; values beyond that can overflow and fire much sooner.
Capture the right loop value
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100);
}
Each let iteration has its own binding, so the output is "1", "2", "3". With var, all callbacks share the same variable. After the loop it is 4, so each logs "4".
Wait for a quiet period
Debouncing cancels a pending timeout each time a new event arrives. A new timeout then schedules the action after the latest event. With a 300 ms delay, typing every 100 ms keeps postponing the action; it runs about 300 ms after typing stops. Store the handle outside the handler so the next call can cancel the previous timer.
Build a debounce handler
let pendingSave;
function scheduleSave(text) {
clearTimeout(pendingSave);
pendingSave = setTimeout(() => {
console.log("Saved: " + text);
}, 300);
}
scheduleSave("n");
scheduleSave("no");
scheduleSave("note");
These calls happen in the same script. Each clears the earlier handle before replacing it, so only "Saved: note" is logged, after about 300 ms. The final callback captures that call’s text parameter. The handle lives outside the function so every call can cancel the previously scheduled timeout.
If the handle were declared afresh inside each call, the next call would not retain the previous timer’s handle. If a handler scheduled without clearing, it would deliver all three messages. Debouncing needs both the shared handle and the cancellation before replacement.
Key Takeaways
- Pass a callback function and measure delays in milliseconds.
- Use the returned handle to clear a timer.
- Timer delays do not guarantee exact execution times.
- An interval repeats until cleared.
- Use let for separate loop bindings.
- Debouncing restarts a delay after each event.
2Write and manage callback-based code
A callback lets a caller supply the next piece of work as a function. That simple idea supports timers, events, and many older APIs. Here you will write a small callback-based operation, follow the result to the code that needs it, and pass failures through an error parameter. You will also see how dependent steps create nested code and why consistent error handling matters at every level.
2.1 Pass callbacks to asynchronous operations
A callback is a function you pass to another function to be called by that function. The callback’s code describes what should happen with a result. Whether it runs during the current call or later depends on the API that receives it.
A callback can run synchronously
const doubled = [2, 5].map(n => n * 2);
console.log(doubled);
map calls its callback for each element before returning the new array. The result is [4, 10]. The arrow function is a callback, but this code does not schedule future work. You can use doubled on the next line because the array is already complete.
Deliver a delayed result
function prepareLabel(name, callback) {
setTimeout(() => {
callback("Label: " + name);
}, 200);
}
prepareLabel("Mira", label => {
console.log(label);
});
console.log("Label requested");
prepareLabel starts a timer and returns. The script logs "Label requested". After about 200 ms, the timer callback constructs the label and calls the supplied callback, which logs "Label: Mira". The operation chooses when to call; the caller chooses what to do with the result.
Put dependent work where the result exists
let label;
prepareLabel("Mira", value => {
label = value;
});
console.log(label);
This logs undefined: the assignment has not happened yet. Move console.log(value) into the callback. Adding a longer unrelated timeout before reading label would only guess when the result will be available; using the result callback ties the work to actual completion.
A return inside a delayed callback also cannot return the value from the outer function. The outer call is already over. In the example, return "Label: Mira" inside the timer callback would return to the timer machinery, leaving the caller of prepareLabel without the delayed label.
Pass the next action without calling it
A named callback works too:
function showLabel(label) {
console.log(label);
}
prepareLabel("Mira", showLabel);
showLabel is the function value. showLabel() would call it immediately with no label and pass its return value instead. Use an arrow wrapper when the later action needs a particular argument or several statements.
For two dependent asynchronous steps, start the second inside the first step’s callback. For example, after receiving a user, pass that user to an operation that prepares their settings. Starting both outside their callbacks would not establish that dependency.
The receiving function controls how often it calls the callback. Some callbacks, such as event handlers, are deliberately called many times. A one-result operation should call its result callback once. A promise gives such an operation a result that can settle only once.
Key Takeaways
- A callback’s timing comes from the API that calls it.
- map uses synchronous callbacks; a timer callback runs later.
- Use a delayed result inside its callback or code called from it.
- Returning from a callback does not return from its outer function.
2.2 Handle errors in callbacks and avoid callback hell
An asynchronous operation can fail as well as succeed. A callback API needs a clear way to report each outcome and a clear place for the caller to handle it.
Check the error first
The error-first convention uses callback(err, result). A successful call passes null for the error. A failed call passes an Error object and does not supply a usable result.
function readLabel(id, callback) {
setTimeout(() => {
if (id <= 0) {
callback(new Error("Invalid label id"));
return;
}
callback(null, { id: id, text: "Draft" });
}, 200);
}
readLabel(4, (err, label) => {
if (err) {
console.log(err.message);
return;
}
console.log(label.text);
});
The producer stops after reporting an invalid ID, so it cannot also report success. The consumer stops after seeing an error, so it cannot try to read label.text from an absent result. With ID 4, the output is "Draft". With ID 0, it is "Invalid label id".
Both returns matter. Without the consumer’s return, a second error about an undefined value could hide the useful original message. Without the producer’s return, one operation could call the callback twice. For a one-result API, report exactly one outcome.
A surrounding try has already finished
try {
setTimeout(() => {
throw new Error("Late failure");
}, 0);
} catch (err) {
console.log("Handled");
}
The try schedules the timer and finishes. The callback throws in a later task, when that try is no longer active, so "Handled" is not logged. Catch a possible throw inside the callback, or pass a failure through the API’s error parameter. A later promise wrapper must turn that callback error into a rejection explicitly.
Keep dependent steps readable
Callback code becomes hard to follow when each next step appears inside the previous step’s callback. Reading a profile, then its projects, then a project’s tasks creates several levels of nesting. Each operation also needs its own error check. This pattern is often called callback hell or the pyramid of doom.
Named functions can give those steps meaningful names and reduce the visual nesting. Keep error reporting consistent across them: a later step is just as capable of failing as the first one. Promises and async functions offer a flatter way to express the same sequence while keeping failure propagation connected.
Not all callback APIs use an error parameter. Some browser objects have separate success and failure events. For an image, set onload and onerror handlers before assigning src so both outcomes have a handler when loading starts. Read the API’s contract before assuming a parameter named err exists.
Key Takeaways
- An error-first callback receives err before the result.
- Return after reporting or handling an error.
- One-result operations should call their callback exactly once.
- An outer try cannot catch a later callback’s throw.
- Named steps or promise-based code reduce deep nesting.
3Create and use promises
A promise gives an operation one shared object for its eventual result. It makes success and failure explicit and lets you connect dependent steps without nesting each one inside the last. Start by building a promise around a timer. Then attach handlers, follow the new promises they return, and check that each asynchronous step stays connected to the chain that depends on it.
3.1 Explain promise states and create a promise
A promise represents an eventual outcome. A function can return it immediately, letting the caller connect work to a result that does not exist yet.
States and settlement
A promise is pending until it is fulfilled with a value or rejected with a reason. Fulfilled and rejected promises are settled; their outcome cannot change.
“Resolved” is more precise than an everyday synonym for “successful.” A promise resolved with another pending promise follows that promise’s eventual outcome, including rejection. It can therefore be resolved while still pending.
Create a promise around an operation
function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
wait(200).then(() => console.log("Pause finished"));
new Promise calls its executor immediately. That executor registers a timer. When the timer calls resolve, the promise fulfills with undefined, and the registered then handler runs later as a microtask. This helper gives a reusable pause without blocking the thread.
An executor can receive both resolve and reject:
function reserve(quantity) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (quantity <= 8) resolve(8 - quantity);
else reject(new Error("Not enough stock"));
}, 200);
});
}
reserve(3).then(left => console.log(left));
reserve(9).catch(err => console.log(err.message));
For these positive quantities, the first call fulfills with 5 and the second rejects with "Not enough stock". The promise lets the caller receive either outcome through the same returned object.
Creation starts the executor
console.log("before");
const result = new Promise(resolve => {
console.log("creating");
resolve(12);
});
console.log("after");
result.then(value => console.log(value));
The output is "before", "creating", "after", "12". Attaching then does not start this executor. The constructor already ran it; then registers code to consume its outcome.
Only the first call to resolve or reject decides the promise’s fate. Later calls cannot replace it. This also applies when the first resolution adopts another promise whose result is still pending. Calling resolve does not stop the executor’s ordinary code, so use return when a branch should end.
Make every path finish correctly
A synchronous throw in the executor rejects the promise automatically. A throw in a later timer callback is outside that call and is not converted into a rejection. Use reject(new Error("...")) there; otherwise the error can be uncaught while the promise remains pending.
A branch that calls neither function can leave the caller waiting forever. Use Error objects for rejection reasons so handlers can read a message and debugging tools can show useful error information.
Promise.resolve(value) creates a fulfilled promise for a plain value or adopts another promise’s outcome. Promise.reject(error) creates a rejected promise. Attach rejection handling when using the latter.
Follow an adopted outcome
const inner = new Promise(resolve => {
setTimeout(() => resolve("packed"), 250);
});
const outer = new Promise((resolve, reject) => {
resolve(inner);
reject(new Error("Too late to replace it"));
});
outer.then(value => console.log(value));
The inner executor starts its timer during construction. The outer executor then resolves with inner, locking outer to inner’s eventual outcome. The later reject call cannot replace that decision. Outer remains pending until inner fulfills, and its handler logs "packed" after about 250 ms.
If inner rejected instead, outer would follow that rejection. The first call to resolve selected which outcome to follow; it did not guarantee success. This distinction matters when a wrapper passes through another promise instead of supplying a finished plain value.
Key Takeaways
- Pending promises become fulfilled or rejected; settled outcomes stay fixed.
- Resolution can adopt another promise’s eventual outcome.
- The executor runs synchronously during construction.
- Use resolve and reject to connect later callbacks to the promise.
- A late callback throw does not automatically reject its enclosing promise.
3.2 React to promises with then, catch and finally
Promise handlers connect code to success, failure, or either outcome. Each handler registration also returns a new promise, allowing the next piece of code to follow what that handler produces.
Use the handler that fits the outcome
p.then(onFulfilled) receives a fulfilled value. p.catch(onRejected) receives a rejection reason; it is shorthand for p.then(undefined, onRejected). Handlers are functions and run as microtasks, even if p has already settled.
Promise.resolve("draft")
.then(text => console.log(text));
console.log("registered");
The script logs "registered" before the handler logs "draft". An immediately available value does not make the handler synchronous.
Passing a function is essential. then(console.log("draft")) logs "draft" during registration and passes undefined as the handler. Use then(() => console.log("draft")) when the message should be logged later. then(console.log) passes the logging function itself, so it receives the value.
Clean up after success or failure
finally is useful for hiding a loading indicator or enabling a button after an operation settles. Its callback receives no value or error argument.
Promise.resolve(4)
.finally(() => console.log("Cleanup"))
.then(value => console.log(value));
The output is "Cleanup", then "4". Returning an ordinary value from the cleanup callback does not replace 4. A returned promise can delay the next step until cleanup finishes. If cleanup throws or returns a rejected promise, that failure becomes the new chain outcome.
A typical page flow sets a loading state before starting, handles a result in then, reports a failure in catch, and resets the loading state in finally. Put the result-dependent work in then or catch, where the needed argument exists.
Separate subscriptions from a chain
const p = Promise.resolve(4);
p.then(value => value * 2);
p.then(value => console.log(value));
These are two separate subscriptions to p. Both receive 4. The multiplication belongs to the new promise returned by the first then; it does not alter p or feed the second subscription.
Connect them to pass the transformed value:
p.then(value => value * 2)
.then(value => console.log(value));
Now the final handler receives 8. The difference is which promise each handler is attached to.
The returned object is still a promise
In const next = p.then(value => value * 2), next is a new promise, not the number 8. The handler may run after the surrounding script ends, and it may itself return delayed work. Use another handler or await to obtain the value. Treating next as a number immediately loses this timing information.
Key Takeaways
- Use then for values, catch for rejections, and finally for cleanup.
- Pass functions as handlers.
- Every then returns a new promise.
- Separate handlers on one promise receive its original outcome.
- Successful finally cleanup preserves the earlier outcome; failed cleanup replaces it.
3.3 Chain promises into a sequence
A promise chain makes the output of one handler the input of the next. Each link must return the value or promise that later links need.
Pass values through the chain
Promise.resolve(3)
.then(count => count * 4)
.then(total => total + 2)
.then(total => console.log(total));
The first handler receives 3 and returns 12. The second receives 12 and returns 14. The final handler logs "14". An arrow expression returns automatically; a function body with braces needs an explicit return to send a value onward.
Return a promise for a dependent step
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function makeCode() {
return wait(100).then(() => "P4");
}
function makeLabel(code) {
return wait(200).then(() => "Parcel " + code);
}
makeCode()
.then(code => makeLabel(code))
.then(label => console.log(label));
makeCode supplies "P4" after about 100 ms. The next handler returns the promise from makeLabel. The chain follows it, waits about another 200 ms, and passes "Parcel P4" to the final handler. The caller receives a label, not a promise nested inside the value.
Find a broken link
Compare that middle handler with this one:
.then(code => {
makeLabel(code);
})
It starts makeLabel but returns nothing. Its new promise fulfills with undefined without waiting for the label. The next handler therefore receives undefined after the code step, while label preparation continues separately. Errors from that separate work also stop flowing through this outer chain.
Repair it with return makeLabel(code); inside the braces, or remove the braces to return the expression. The same rule applies when a handler starts a pause: .then(() => wait(200)) waits; .then(() => { wait(200); }) does not.
Flatten nesting and keep needed values
Putting another then deep inside the first handler can recreate callback-style nesting. Return the inner promise and put the next handler at the outer level. This preserves one path for timing, values, and errors.
Each handler has its own local scope. If a later step needs both the code and the label, return an object holding them:
makeCode()
.then(code => makeLabel(code).then(label => ({ code, label })))
.then(pair => console.log(pair.code + ": " + pair.label));
This small inner handler builds the combined value and is returned, so it stays connected. An async function can also keep both results in ordinary local variables.
A rejection skips later fulfillment handlers until a rejection handler is reached. A final catch can handle failures from every connected step above it. Dependent work belongs in this sequence; independent work can start together with Promise.all.
Key Takeaways
- Return the value that the next handler needs.
- Returning a promise makes the chain wait for its outcome.
- Braced arrow bodies need return.
- Keep dependent work and its errors connected to the outer chain.
- Pass combined data onward when later steps need earlier local values.
4Write asynchronous code with async and await
Async functions let you write promise-based steps in the order you want them to happen. Values can stay in named variables, and await marks the points where the function pauses. Learn how that pause affects the caller, convert older code into this style, and spot mistakes that leave a promise where a value is needed. Loop behavior deserves particular care because an async callback does not make its caller wait.
4.1 Write async functions and use await
An async function always returns a promise. Inside it, await pauses that function until a value is available, letting dependent steps read from top to bottom.
Return a value through a promise
async function getCount() {
return 6;
}
getCount().then(count => console.log(count));
Calling getCount() gives a promise fulfilled with 6. The handler logs "6". If the function throws, its returned promise rejects. Returning another promise makes the async function follow that promise’s eventual outcome.
You can declare an async function with async function name(), an async arrow such as const name = async () => {}, or an async object method. The promise-returning behavior is the same.
Pause at await
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function showSteps() {
console.log("Step one");
await wait(300);
console.log("Step two");
}
showSteps();
console.log("Caller continues");
The output starts with "Step one", then "Caller continues". About 300 ms later, it logs "Step two". Calling the function runs its body synchronously up to the first await. The pause returns control to the caller. Once the promise fulfills, the rest of showSteps resumes through the microtask queue.
Await pauses its function, not the whole thread. Other ready tasks can run during the wait. A large synchronous calculation before or after the await can still block the thread while that calculation runs.
Read a value or receive an error
A fulfilled promise supplies its value: const count = await getCount(); sets count to 6. A rejected promise throws its reason at the await line. Surround the awaited operation with try and catch when this function should handle that failure.
await also accepts plain values. In const count = await 6;, the result is still 6, but the function’s continuation is deferred. It does not become synchronous just because no waiting operation was needed.
Check where await belongs
Use await inside the async function whose body contains it. It is also allowed at the top level of a JavaScript module. A normal function body cannot contain await; an outer async function does not make a nested normal callback async.
A caller still receives a promise. To use the final value, await it from another async function or attach then. For example, const total = getCount(); stores a promise; it does not store 6.
An event handler can itself be async. The handler can await a preparation step before updating a page. Give that action its own error handling because event dispatch does not wait for the handler’s returned promise. At the top of a program, a call such as showSteps().catch(console.error) gives an unanticipated failure a handler.
Key Takeaways
- An async function returns a promise for its return value or thrown error.
- Code before the first await runs during the call.
- Await resumes later with a value or throws a rejection reason.
- Await belongs inside an async function or at module top level.
4.2 Rewrite promise chains and callback APIs with async and await
A promise chain can become an async function without changing its dependencies. Each result gets a local variable, and each awaited step begins only after the earlier result it needs is ready.
Replace handlers with awaited assignments
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getUser(id) {
return wait(100).then(() => ({ id, name: "Nia" }));
}
function getOrders(user) {
return wait(200).then(() => [user.name + "-1", user.name + "-2"]);
}
async function showOrders(id) {
const user = await getUser(id);
const orders = await getOrders(user);
console.log(user.name + ": " + orders.length + " orders");
}
showOrders(8).catch(console.error);
The first await supplies a user. Only then can getOrders(user) start. After about 300 ms in total, the function logs "Nia: 2 orders". Both user and orders are in scope for that final line. In a chain, each handler’s parameter would belong to a separate local scope.
The corresponding chain would return getOrders(user) from the first handler. Rewriting it with await makes that wait explicit while keeping the order intact. A final chain catch becomes a try/catch around the awaited steps; finally becomes a cleanup block.
Convert a callback API into a promise API
Adding async to a wrapper does not make a callback API’s delayed value become its return value. Connect the callback to promise settlement:
function readTag(id, callback) {
setTimeout(() => {
if (id <= 0) callback(new Error("Invalid tag"));
else callback(null, "Tag " + id);
}, 100);
}
function readTagAsync(id) {
return new Promise((resolve, reject) => {
readTag(id, (err, tag) => {
if (err) {
reject(err);
return;
}
resolve(tag);
});
});
}
Inside an async function, const tag = await readTagAsync(8); gives "Tag 8". Calling it with 0 instead throws "Invalid tag" at the await. Success resolves the wrapper; failure rejects it. The adapter changes how the outcome is delivered, not what the underlying operation does.
This conversion is called promisifying. For ordinary one-result error-first APIs, Node.js also provides util.promisify, and many built-in modules already have promise-based variants such as fs/promises.
Keep one clear path
Timers become pauses through a promise-returning wait helper. An async loop can log a step, await the pause, and then begin the next iteration. With callbacks, the next step would have to be scheduled inside the previous callback.
Promises, chains, and async functions work together: an async function can await a chain, and a caller can attach then to an async function’s returned promise. Within a function, use the form that makes dependencies and error handling easy to follow. An extra Promise constructor is useful for adapting a callback, but existing promise-returning operations can be awaited directly.
Key Takeaways
- Each dependent chain step can become an awaited assignment.
- Local variables keep earlier results available later in the async function.
- Promisify callbacks by mapping errors to reject and results to resolve.
- Existing promise APIs can be awaited directly.
4.3 Avoid common async and await mistakes
Async code can look synchronous while still passing promises between functions. Many bugs come from reading the eventual value too early or assuming a helper waits for callbacks that it ignores.
Await the value before using it
async function getQuantity() {
return 18;
}
async function total() {
const quantity = await getQuantity();
console.log(quantity * 3);
}
total().catch(console.error);
This logs "54". Without await, quantity would hold a promise. Multiplying that promise by 3 produces NaN, not a delayed calculation that automatically updates itself.
A condition has the same trap. An async check returning false still returns a promise object, and objects are truthy. Use if (await allowed()) inside an async function when the branch depends on the eventual boolean. Testing if (allowed()) tests the object itself.
forEach does not collect callback promises
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function labelAll(ids) {
ids.forEach(async id => {
await wait(100);
console.log("Label " + id);
});
console.log("Labels finished");
}
labelAll([4, 9]).catch(console.error);
"Labels finished" appears before the labels. forEach calls both callbacks, ignores their returned promises, and finishes its own synchronous iteration. Even await ids.forEach(...) would await undefined, not the callback work. A failure inside those ignored promises is also outside the promise returned by labelAll.
For ordered processing, write an ordinary loop:
for (const id of ids) {
await labelOne(id);
}
console.log("Labels finished");
Here labelOne is a promise-returning operation. Each item starts after the previous one finishes. For independent items, use await Promise.all(ids.map(id => labelOne(id))); before the final log. map collects the returned promises, and Promise.all gives the caller one promise for their completion.
Separate independence from syntax
Awaiting each operation immediately makes their waits sequential. For independent waits of 1000, 2000, and 1500 ms, that takes about 4500 ms. Starting all three and combining them takes about 2000 ms. A step that needs an earlier result still has to wait for it.
Keep failures connected
A nested function needs its own async keyword to use await, but that keyword alone does not make its caller await the result. Check both ends: does the callback return a promise, and does the caller use it?
For work deliberately started without awaiting completion, attach a rejection handler such as backgroundTask().catch(console.error). Also check returns in async wrappers: async function prepare() { await wait(100); } fulfills with undefined. Waiting for a step and returning its result are separate decisions.
Return results from the repaired group
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function prepareItem(id) {
await wait(50);
return "Item " + id;
}
async function prepareAll(ids) {
return await Promise.all(ids.map(id => prepareItem(id)));
}
prepareAll([6, 2]).then(items => console.log(items.join(", ")));
Each map callback returns a promise, and map keeps those promises in an array. All waits for both and supplies ["Item 6", "Item 2"]. PrepareAll returns that array through its own promise, so the caller logs "Item 6, Item 2" after the group finishes.
Removing prepareAll’s return would still wait for the group, but the caller would receive undefined. Replacing map with forEach would lose the array of promises. Check collection, waiting, and returning separately when repairing a multi-item action.
Key Takeaways
- Await before using an eventual number or boolean.
- forEach ignores promises returned by async callbacks.
- Use for...of for sequential work or map with Promise.all for independent work.
- A nested callback needs its own async keyword.
- Keep deliberately unawaited work attached to a rejection handler.
5Handle errors in asynchronous code
A failed operation is part of the program’s control flow. The code must decide whether to recover, retry, report the failure, or pass it to a caller that can decide. Follow failures through promise chains and through awaited calls, then use cleanup code that runs on either outcome. Finally, put error handling at the boundary of an action so that the user gets a useful message and you keep the technical cause.
5.1 Handle rejections in promise chains
A promise rejection follows the connected chain until a rejection handler deals with it. Understanding what that handler returns tells you whether the chain has recovered or is still failing.
Trace a rejection past success handlers
Promise.resolve(8)
.then(value => {
throw new Error("Count rejected");
})
.then(value => console.log("Unused: " + value))
.catch(err => {
console.log(err.message);
return 2;
})
.then(value => console.log("Using " + value));
The throw rejects the new promise returned by the first then. The next fulfillment handler is skipped, so "Unused" is never logged. The catch logs "Count rejected" and returns 2. That return fulfills the next link, so the last handler logs "Using 2".
An explicitly rejected promise and a throw inside a handler enter the same rejection path. This is why one final catch can handle failures from several earlier connected steps.
Choose recovery or propagation
A rejection handler that returns a value recovers with that value. Returning nothing also recovers, but with undefined. Logging a failure is therefore not the same as keeping the chain rejected:
Promise.reject(new Error("Label missing"))
.catch(err => console.log(err.message))
.then(value => console.log(value));
The final handler receives undefined. If later code requires a real label, that silent recovery is probably wrong. Return a usable fallback, or throw the error again.
function reportAndRethrow(err) {
console.error(err);
throw err;
}
Using this function in catch logs the error and leaves the next promise rejected with the same reason. A caller further down the chain can decide what to do.
Put the handler after the work it protects
A catch handles failures above it. If it recovers, later steps run normally, and their new failures need a later handler. A handler in the middle cannot catch errors that have not happened yet.
There is also an important difference between .then(success, failure) and .then(success).catch(failure). The two handlers in one then respond to the outcome of the incoming promise. Its failure handler does not catch an error thrown by its own success handler. That error belongs to the new promise the call returns, so a downstream catch is needed.
Preserve useful errors
Reject with Error objects so handlers can read err.message and err.name. Throwing or rejecting a string is allowed, but code expecting an Error’s fields will not receive them. Keep the original error when propagating a failure unless added context helps the caller understand which operation failed.
Key Takeaways
- Rejections skip fulfillment handlers until a rejection handler is reached.
- Returning from catch recovers; returning nothing recovers with undefined.
- Rethrow to keep a failure traveling downstream.
- A catch protects connected steps before it.
- A then rejection handler cannot catch its sibling success handler’s throw.
5.2 Use try, catch and finally with await
try, catch, and finally let an async function handle failures at the point where it awaits them. Use this structure to keep result handling, recovery, and cleanup together.
An error transfers control
An Error object describes a failure. new Error("Label missing") has the message "Label missing" and the name "Error"; debugging tools can also show its stack. Built-in failures have types such as TypeError.
throw stops normal execution at that point and transfers control to the nearest applicable catch. Code after the handling can still run when the function catches its own error. Throw Error objects so later handlers receive useful fields.
Await inside the protected block
async function run() {
console.log("Working");
try {
await Promise.reject(new Error("Device busy"));
console.log("Complete");
} catch (err) {
console.log("Failed: " + err.message);
} finally {
console.log("Controls enabled");
}
}
run().catch(console.error);
The rejected promise throws at the await line. The rest of try is skipped. The output is "Working", "Failed: Device busy", then "Controls enabled". A successful operation would reach "Complete" and still run the same cleanup.
Use finally for a spinner, button state, or other resource that must be reset after either outcome. It runs even when try or catch has a return statement. Keep cleanup from throwing unless that new failure is deliberately meant to replace the earlier outcome. A return in finally can override an earlier return or throw, so avoid using it for ordinary cleanup.
Returning a promise is different from awaiting it here
async function readSetting() {
try {
return await Promise.reject(new Error("Missing setting"));
} catch {
return "light";
}
}
readSetting().then(value => console.log(value));
This logs "light". The function waits inside try, so rejection reaches the local catch. Removing await returns the rejected promise immediately; the function adopts its rejection, but the local catch does not handle that later outcome. A caller would need to catch it.
Give each failure an appropriate boundary
One try can cover a group of awaits when the whole action should stop if any step fails. Give an optional step a smaller boundary when it can recover independently. For example, const settings = await loadSettings().catch(() => ({ theme: "light" })); uses a default object and continues.
A variable declared inside try is block-scoped. If code after the block needs it, declare let result; before try, or finish the result-dependent work inside try. Do not move a value outside its scope merely to keep a final log in a preferred position.
When the current function cannot recover, allow rejection to reach the caller. Catching it only to hide it prevents that caller from making a useful decision.
Key Takeaways
- Awaited rejections throw at the await line.
- Catch can recover, report, or rethrow.
- Finally runs after either outcome, including a return.
- Use return await inside try when local catch must handle the returned operation’s rejection.
- Block-scoped results stay inside their try block.
5.3 Keep failures visible and recover gracefully
An action needs an error boundary: a place that receives its final failure and chooses a response. Without that boundary, a promise can reject while nobody is handling its outcome.
Handle the final path
An unhandled rejection is a rejection that still lacks a handler when the host checks. Handling can use catch, the rejection argument of then, or try/catch around await. A browser reports the problem and can fire unhandledrejection. Node.js stops by default for unhandled rejections under its normal policy, so leaving one loose can end a program.
An action such as main() or an async click handler should handle its final promise. Lower-level functions can pass failures upward, but the path must end somewhere that decides what to do. A global rejection listener can help log missed failures; it cannot choose the best recovery for each action.
An empty catch swallows the failure. Log technical details with console.error(err) and show a clear message if the user’s action failed. A message such as "The label could not be saved. Try again in a moment." gives the user an action without exposing a stack trace.
Add context without losing the cause
async function prepareLabel() {
try {
return await Promise.reject(new Error("Device offline"));
} catch (err) {
throw new Error("Could not prepare label", { cause: err });
}
}
prepareLabel().catch(err => {
console.log(err.message);
console.log(err.cause.message);
});
This logs "Could not prepare label" and "Device offline". The outer message identifies the failed action. The cause property preserves the lower-level error for diagnosis. The Error cause option is part of ECMAScript 2022.
Retry a fresh operation, with a limit
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function withRetry(task, attempts) {
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await task();
} catch (err) {
if (attempt === attempts) throw err;
await wait(200);
}
}
}
Call this with a function that starts a new attempt and a positive whole-number limit. If task fails twice and succeeds on its third call, a limit of four stops on the third call and returns its value. If all four attempts fail, the fourth error is rethrown. The delay occurs only between attempts.
return await keeps a rejection inside this try, where catch can retry it. Passing one already rejected promise instead of a task function would not restart the work.
Retry when a failure can be temporary. Use a fallback when the feature can work with default data. Stop and inform the user when the action cannot succeed. If a lower-level function cannot make that choice, preserve the error and let the caller decide.
Key Takeaways
- Every asynchronous action needs a final failure handler.
- An empty catch hides useful failure information.
- An Error cause preserves the original failure while adding context.
- Retry by calling a task again, with a finite attempt limit.
- Choose retry, fallback, stopping, or propagation to suit the failure.
6Run asynchronous tasks in parallel
Some operations need earlier results; others can begin together. Identify that dependency before choosing where to put await. You will combine independent operations, keep results in the right order, and choose whether your program needs every success, every outcome, the first settlement, or the first success. Each choice changes what a failure means and how long the caller waits.
6.1 Start tasks together and wait with Promise.all
Independent asynchronous operations can wait at the same time. The choice to start them together comes from their data needs, not from adding the word async.
Start independent operations before waiting for all
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function makePart(name, ms) {
return wait(ms).then(() => name);
}
async function assemble() {
const parts = await Promise.all([
makePart("cover", 1000),
makePart("pages", 2000),
makePart("label", 1500)
]);
console.log(parts.join(", "));
}
assemble().catch(console.error);
The three calls start before Promise.all waits. About 2000 ms later, the array contains ["cover", "pages", "label"]. Waiting for each call before starting the next would take about 4500 ms instead. Timer delays are approximate, so real elapsed times can be longer.
Here parallel means overlapping waits. The promise handlers still run one at a time on the main thread. Calling three heavy synchronous functions inside the array would run those calculations one after another; Promise.all would not move them to separate threads.
Results keep their input positions
The label finishes before the pages, but remains third in the array. Promise.all keeps input order, not completion order. Destructuring can name those positions:
const [cover, pages] = await Promise.all([
makePart("cover", 1000),
makePart("pages", 2000)
]);
Use that code inside an async function. For a collection, ids.map(id => loadItem(id)) starts one promise-returning operation per ID and gives Promise.all the promise array. Do not await each item inside a loop if all are independent and should overlap.
Failure ends the combined wait
Promise.all rejects as soon as an input rejects. If one part fails after about 300 ms, a catch can receive that reason then; it need not wait for the longest successful part.
The other operations keep running. Their side effects can still happen, but the combined promise will not later fulfill with their values. This fail-fast rule is useful when the next step requires every result.
Respect dependencies and attach handlers promptly
If one operation needs a user produced by another, obtain the user first. The later calls can be combined only if they are independent of one another once that user exists.
Starting two promises and awaiting them separately does overlap their waits, but the second can reject before the code reaches its await. If it still has no handler when the host checks, an unhandled rejection can be reported. Combining the promises promptly with Promise.all attaches handling to both inputs and gives the caller one failure path.
Plain values are allowed among the inputs and keep their positions. Promise.all([]) is already fulfilled with an empty array. A handler attached to it still runs asynchronously.
Key Takeaways
- Start independent operations together to overlap their waits.
- Promise.all returns results in input order.
- One rejection rejects the combined promise without canceling other inputs.
- Dependent steps still need their earlier results.
- Combine started promises promptly to keep their error paths connected.
6.2 Choose between Promise.allSettled, Promise.race and Promise.any
The right promise combinator depends on what counts as a useful result. Decide whether you need all successes, all outcomes, the first settlement, or the first success.
Keep partial results with allSettled
Promise.allSettled waits for every input and returns outcome records in input order. A success becomes { status: "fulfilled", value }; a failure becomes { status: "rejected", reason }. A rejected input does not reject the combined promise.
Promise.allSettled([
Promise.resolve("Map ready"),
Promise.reject(new Error("Chart unavailable"))
]).then(results => {
for (const result of results) {
if (result.status === "fulfilled") console.log(result.value);
else console.log(result.reason.message);
}
});
This logs "Map ready" and "Chart unavailable". It suits independent panels where one working panel remains useful even if another fails. Read value only on a fulfilled record and reason on a rejected record.
Let the first settlement decide with race
Promise.race follows the first input that settles, whether it fulfills or rejects. A failure after 100 ms beats a success after 200 ms. This is useful for a time limit:
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("Time limit reached")), ms);
});
}
Promise.race([
wait(600).then(() => "Prepared"),
timeout(250)
]).then(console.log).catch(err => console.log(err.message));
The timeout rejects first, so the output is "Time limit reached". The 600 ms operation keeps running. A race settles the caller’s combined promise; it does not cancel its losing operations.
Wait for the first success with any
Promise.any ignores failed inputs while a success is still possible. If one operation fails after 100 ms and another fulfills after 200 ms, it gives the latter value. A faster rejection does not win.
If every input rejects, any rejects with an AggregateError. Its errors array keeps the rejection reasons in input order. This allows a caller to inspect why each attempt failed, even when their failures arrived in a different order.
Compare the decision rules
all: every value is required; reject on the first rejection.allSettled: every outcome is required; wait for all records.race: first settlement decides, including failure.any: first fulfillment decides; reject only if all reject.
With no inputs, all and allSettled fulfill with []. any rejects with AggregateError because no success is possible. race stays pending because nothing can settle it.
All four are supported in current browsers and Node.js. all and race entered ECMAScript in 2015, allSettled in 2020, and any in 2021.
Compare one pair of outcomes
Suppose both operations start now: the first input fails after about 100 ms, while the second fulfills with "usable" after about 300 ms. Applying each combinator to that same pair gives different results:
- All rejects near 100 ms with the first input’s error, because every input must succeed.
- AllSettled fulfills near 300 ms with a rejected record followed by a fulfilled record, preserving input positions.
- Race rejects near 100 ms because that is the first settlement.
- Any fulfills near 300 ms with "usable" because the earlier failure cannot supply a success.
All and race happen to reject together in this case, but they use different rules. If the fast first input succeeded and the second input failed later, race would keep its early success while all would reject on the later failure. Choose from the required outcome, not from the timing of one example.
Key Takeaways
- allSettled reports every success and failure in input order.
- race follows the first settlement, including rejection.
- any accepts the first success and rejects only when every input rejects.
- A race timeout leaves losing work running.
- Empty race input stays pending; empty any input rejects.
- Choose the combinator from the outcome the caller actually needs.
Asynchronous JavaScript examples
1Predict what runs now and what runs later
14 steps
- 1IntroductionSeparate starting from finishing
Start by separating immediate calls from registered callbacks. A timer can wait without occupying the main thread, but its callback still needs that thread. Trace what runs during the script on the call stack before moving on to queued work, a task or a microtask, because their order decides what you see first.
- 2ExampleRegister a callback
Read this script before predicting its output:
console.log("request"); setTimeout(() => console.log("ready"), 200); console.log("continue");The first log runs now. The timer call registers a function and returns, so the script reaches the final log immediately. The callback’s log belongs to a later task; creating that function has not called it.
- 3Key InsightWaiting does not occupy the thread
The output begins "request", "continue", followed by "ready" after about 200 ms. The host manages the wait, but the callback still needs the main thread. A long loop can delay it even after the timer becomes ready.
- 4ChallengeCan the timer interrupt?
A script registers a zero-delay timer and then performs a long synchronous calculation. The timer becomes ready before the calculation finishes. Can its callback run during that calculation?
---reveal---
No. The calculation keeps the main thread until its current work finishes. Becoming ready queues the callback for later; it does not give the callback a separate execution thread or permission to interrupt the calculation.
- 5ExplanationKeep two kinds of later work
A timer callback runs as a task. A promise handler runs as a microtask. The call stack records the calls currently executing.
After the current work, a microtask checkpoint drains the microtask queue before another task can start. Any microtasks added during the drain join that same queue. To trace a script, keep separate lists for immediate output, microtasks, and timer tasks.
- 6ExampleMark the queues
Now add a promise handler:
console.log("request"); setTimeout(() => console.log("timer"), 0); Promise.resolve().then(() => console.log("job")); console.log("continue");The immediate output is "request", "continue". The promise handler is waiting as a microtask; the timer callback is a task. Finish the current script before running either registered function.
- 7ExampleDrain before the next task
After the script finishes, the microtask logs "job". Only after the microtask queue is empty can the ready timer task log "timer".
The complete output is "request", "continue", "job", "timer". A fulfilled promise does not call its handler synchronously, and a zero-delay timer does not jump ahead of microtasks. The browser can render at a rendering opportunity after this drain.
- 8ChallengePlace a newly queued job
What is the output order?
Promise.resolve().then(() => console.log("a")) .then(() => console.log("c")); queueMicrotask(() => console.log("b")); console.log("now");---reveal---
"now", "a", "b", "c". The script queues a before b. Only when a’s handler returns does the next chain handler become ready, so c joins behind the b job that was already queued.
- 9ExplanationChoose a timer and keep its handle
Use
setTimeoutfor one scheduled call andsetIntervalfor repeated calls. Both return a handle you can save.const hint = setTimeout(() => console.log("Hint"), 800); clearTimeout(hint);This cancels the callback before it runs. For an interval, pass its handle to
clearIntervalwhen repetition should end. Pass a function to schedule; calling it in the argument list executes it immediately. - 10TipAllow for late callbacks
Delays use milliseconds and give a minimum wait. Busy code, background tabs, and browser limits can delay callbacks further. Schedule a nested timeout after the current run; await its asynchronous work too when runs must not overlap.
- 11ExampleUse the right loop binding
With
let, each iteration gives its callback a separate index:for (let n = 0; n < 3; n++) { setTimeout(() => console.log(n), 50); }The callbacks log "0", "1", "2". Replacing let with var makes them share one variable, whose value is 3 after the loop. All three then log "3". The callback reads the binding when it runs later.
- 12ChallengeRestart a quiet-period timer
A handler uses a 300 ms debounce delay. Input events occur at 0 ms, 80 ms, and 170 ms, and there are no more events. About when does its one action run?
---reveal---
About 470 ms after the first event: 300 ms after the last event at 170 ms. Each event cancels the previous pending timeout and schedules a new one. The earlier scheduled actions do not also run, and a busy thread could delay the final action further.
- 13ChallengeConnect the timing rules
A timer is ready, but each microtask keeps queuing another microtask. Why can neither the timer callback nor the next screen update proceed?
---reveal---
The microtask queue never finishes draining, so the event loop cannot move on to the next task or rendering opportunity. The timer’s completed delay does not override that order. Scheduling endless microtasks can block progress just as a long loop can.
- 14RecapA trace you can reuse
Trace immediate code first, drain microtasks next, then move to ready tasks. Keep timer handles for cancellation. Delays are approximate; let retains iteration values, while debouncing restarts a quiet-period delay.
2Deliver a result through a callback
9 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionGive the caller a next action
- 2ExampleProduce the delayed value
- 3ConnectionThe API controls timing
- 4ChallengeReturn from which function?
- 5ExampleReport one of two outcomes
- 6TipStop before reading a failed result
- 7ChallengeLocate the active error boundary
- 8ChallengeConnect results and failures across steps
- 9RecapFollow each outcome to its consumer
3Build and connect promise outcomes
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionOne object for a later outcome
- 2ExampleBuild a reusable pause
- 3Key InsightKeep state and resolution distinct
- 4ChallengeCatch a late failure correctly
- 5ExplanationAttach the right handler
- 6ExampleTwo subscriptions keep one value
- 7ChallengeRead a cleanup result
- 8ExampleBuild the first link
- 9ExampleMake the next link wait
- 10ChallengeRepair a detached operation
- 11ChallengeConnect cleanup to a dependent chain
- 12RecapCheck the returned path
4Rewrite a sequence with async and await
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionKeep promises, change the reading order
- 2ExampleAn async call begins immediately
- 3ChallengeRead the returned value
- 4ExampleConnect a callback to a promise
- 5ExampleKeep intermediate values in scope
- 6ChallengeDoes async alone adapt the result?
- 7Key InsightTest the value, not its promise
- 8ExampleforEach does not await callbacks
- 9TipChoose one loop policy
- 10ChallengeRepair early completion
- 11ChallengeConnect adaptation and the loop
- 12RecapInspect both sides of an async call
5Keep failure, recovery, and cleanup connected
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionDecide what a failure should mean
- 2ExampleWatch a catch recover
- 3Key InsightLogging chooses a return too
- 4ChallengeKeep the failed outcome
- 5ExampleAwait where the catch can see failure
- 6TipReturn after local handling
- 7ChallengeChoose the local fallback
- 8ConnectionEnd the action’s error path
- 9ExampleKeep context and the original cause
- 10ExampleRetry a new call, with a limit
- 11ChallengeStop after success
- 12ChallengeConnect recovery to cleanup
- 13RecapGive every outcome a destination
6Choose what a group of tasks must deliver
11 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionStart with the dependency question
- 2ExampleStart both operations together
- 3Key InsightFailure does not cancel the group
- 4ChallengeRespect one dependency, overlap the rest
- 5ExplanationKeep every outcome when parts remain useful
- 6ExampleSeparate first settlement from first success
- 7ChallengeChoose for a partial page
- 8TipA timeout ends waiting, not the operation
- 9ChallengeWhat can settle an empty group?
- 10ChallengeConnect start timing and failure handling
- 11RecapChoose the outcome policy
Asynchronous JavaScript practice questions
Set 1: Follow the code from calls to callbacks10 Practice Questions
Question 1 of 10
Show answerHide answer
Question 2 of 10
What is the output order?
console.log("open");
setTimeout(() => console.log("tick"), 0);
Promise.resolve().then(() => console.log("job"));
console.log("close");
Show answerHide answer
Question 3 of 10
const reminder = setTimeout(showHint, 900);. What statement cancels it before it runs?Show answerHide answer
Question 4 of 10
What does this loop log after its callbacks run?
for (var index = 2; index < 5; index++) {
setTimeout(() => console.log(index), 0);
}
Show answerHide answer
Question 5 of 10
How many callback calls have completed when this code reaches the log?
let calls = 0;
[3, 6, 9].map(n => {
calls++;
return n + 1;
});
console.log(calls);
Show answerHide answer
Question 6 of 10
What does result contain when the log runs?
function make() {
setTimeout(() => { return "badge"; }, 0);
}
const result = make();
console.log(result);
Show answerHide answer
Question 7 of 10
An error-first callback gets an Error and no result on failure. What prevents success code after this error check from reading an absent item?
if (err) {
console.log(err.message);
// Add one statement here.
}
console.log(item.name);
Show answerHide answer
Question 8 of 10
What is the output order?
Promise.resolve().then(() => {
console.log("oak");
queueMicrotask(() => console.log("elm"));
}).then(() => console.log("ash"));
queueMicrotask(() => console.log("fir"));
console.log("pine");
Show answerHide answer
Question 9 of 10
A one-result operation uses this timer body. What happens when id is 0, and what edit fixes it?
if (id <= 0) callback(new Error("Invalid"));
callback(null, { id });
Show answerHide answer
Question 10 of 10
What is the output order?
console.log("outside");
const p = new Promise(resolve => {
console.log("executor");
resolve("value");
});
p.then(console.log);
console.log("registered");
Show answerHide answer
Set 2: Pass values through promises and await10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
What is logged?
const p = Promise.resolve(7);
p.then(n => n + 4);
p.then(n => console.log(n));
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
What is logged?
Promise.resolve(6)
.then(n => n - 2)
.then(n => n * 5)
.then(console.log);
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
What value does this promise supply to its fulfillment handler?
const p = new Promise((resolve, reject) => {
resolve(14);
reject(new Error("Changed"));
resolve(28);
});
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
A promise-returning wait(ms) fulfills after about ms milliseconds. What reaches console.log, and when?
wait(120)
.then(() => { wait(480).then(() => "ready"); })
.then(console.log);
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
What kind of value does calling this function return?
async function count() { return 9; }
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
What is the output order?
async function show() {
console.log("inside");
await 0;
console.log("after await");
}
show();
console.log("caller");
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
done(err, value) once. What should its promise wrapper do when err is an Error?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
What is logged?
async function permitted() { return false; }
if (permitted()) console.log("enter");
else console.log("stop");
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
record(id) returns a promise and may reject. How should this function process IDs in order, stop on a failure, and let its caller catch that failure?
async function saveAll(ids) {
ids.forEach(async id => { await record(id); });
}
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
What value does the last handler log?
Promise.resolve(11)
.finally(() => 40)
.then(n => n + 2)
.finally(() => Promise.resolve(90))
.then(console.log);
Show answerHide answer
Save this lesson to see this answer.
Set 3: Debug complete asynchronous flows10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
What does the final handler log?
Promise.reject(new Error("first"))
.catch(() => 4)
.then(n => { throw new Error("second " + n); })
.finally(() => 9)
.catch(err => console.log(err.message));
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
What runs first when this statement is evaluated?
setTimeout(showMessage(), 500);
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
inner is still pending. What happens here if inner later rejects?
const outer = new Promise((resolve, reject) => {
resolve(inner);
reject(new Error("replacement"));
});
outer.catch(err => console.log(err.message));
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
What is the output order?
async function step() {
console.log("a");
await null;
console.log("b");
queueMicrotask(() => console.log("c"));
}
setTimeout(() => console.log("d"), 0);
step();
Promise.resolve().then(() => console.log("e"));
console.log("f");
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Why does this retry function not retry an asynchronously rejected task?
async function retry(task) {
for (let n = 0; n < 3; n++) {
try { return task(); }
catch (err) { if (n === 2) throw err; }
}
}
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Promise.race([]) when no inputs will ever be added?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Promise.any([first, second]) eventually provide to catch?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
What does the caller log?
async function value() {
try {
return await Promise.reject(new Error("lost"));
} catch {
return "fallback";
} finally {
console.log("cleanup");
}
}
value().then(console.log);
console.log("caller");
Show answerHide answer
Save this lesson to see this answer.
Set 4: Handle failures and combine independent tasks10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
new Error("Report failed", { cause: original }). What property keeps the original Error available?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
What does the caller log?
async function read() {
try {
return Promise.reject(new Error("missing"));
} catch {
return "local";
}
}
read().then(console.log).catch(err => console.log(err.message));
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
"default" after receiving a rejection. What does the following then handler receive?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
What is logged?
Promise.resolve("start")
.then(() => { throw new Error("later"); },
() => console.log("same call"))
.catch(err => console.log(err.message));
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
What is the output order?
async function run() {
try {
await Promise.reject(new Error("busy"));
console.log("saved");
} catch (err) {
console.log(err.message);
} finally {
console.log("reset");
}
}
run();
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
"ok" on call 3. What happens?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
await Promise.all(...) and both succeed. About how long does the combined wait take?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Promise.all([A, B, C]) do?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
"backup" after about 400 ms. How do race and any differ?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
AggregateError
Async function
Asynchronous callback
Asynchronous operation
Await
B
Blocking
C
Call stack
Callback
Callback hell
Catch boundary
Concurrency
Continuation
D
Debouncing
Dependent operation
E
Early return
Error cause
Error event
Error object
Error-first callback
Event loop
Executor
F
Fail-fast behavior
Fallback
Finally block
Finally callback
Flat chain
Forgotten await
Fulfilled
Fulfillment handler
H
Host environment
I
Ignored callback promise
Independent operations
Input order
Intermediate result
Interval
M
Main thread
Microtask
Microtask starvation
Missing return
N
Nested timeout
P
Pending
Promise
Promise adoption
Promise chain
Promise subscription
Promise wrapper
Promise.all
Promise.allSettled
Promise.any
Promise.race
Promisification
R
Recovery
Rejected
Rejection handler
Rejection propagation
Resolved
Rethrow
Retry
Run to completion
S
Sequential processing
Settled
Swallowed error
Synchronous callback
map doubles each element before the completed result array is returned.Synchronous code
T
Task
Throw
Timeout
Timer handle
Top-level await
Truthy value
Try block
U
Unhandled rejection
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.