Asynchronous JavaScript

Learn how JavaScript handles work that takes time without freezing the page. Follow the event loop to predict the order in which code runs, schedule work with timers, and write asynchronous code in all three styles: callbacks, promises and async functions with 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

  1. 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 code
      Explain 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 loop
      Trace 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 timers
      Run 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.
  2. 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 operations
      Write 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 hell
      Use 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.
  3. 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 promise
      Describe 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 finally
      Register 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 sequence
      Return values and promises from handlers to build flat chains of dependent steps, and avoid the missing-return and nesting mistakes.
  4. 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 await
      Declare 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 await
      Turn 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 mistakes
      Recognize and fix forgotten awaits, async callbacks in forEach, promises used as conditions and async calls whose errors go unhandled.
  5. 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 chains
      Follow 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 await
      Use 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 gracefully
      Prevent unhandled rejections, avoid swallowing errors, add context to errors, and choose between retrying, using a fallback and informing the user.
  6. 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.all
      Run 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.any
      Use 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.

Asynchronous JavaScript study guide

6 chapters

1

Explain 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");
  1. The script logs "begin".
  2. It registers a timer callback; it does not call it.
  3. It queues the fulfilled promise’s handler as a microtask.
  4. It logs "finish" and finishes the current work.
  5. The microtask logs "promise".
  6. 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.
2

Write 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.
3

Create 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.
4

Write 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.
5

Handle 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.
6

Run 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

6 Walkthroughs

1

Predict what runs now and what runs later

14 steps
Follow a small script through the stack and queues, then use the same timing model to schedule and cancel timers.
  1. 1
    IntroductionSeparate 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.

  2. 2
    ExampleRegister 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.

  3. 3
    Key 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.

  4. 4
    ChallengeCan 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.

  5. 5
    ExplanationKeep 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.

  6. 6
    ExampleMark 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.

  7. 7
    ExampleDrain 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.

  8. 8
    ChallengePlace 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.

  9. 9
    ExplanationChoose a timer and keep its handle

    Use setTimeout for one scheduled call and setInterval for 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 clearInterval when repetition should end. Pass a function to schedule; calling it in the argument list executes it immediately.

  10. 10
    TipAllow 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.

  11. 11
    ExampleUse 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.

  12. 12
    ChallengeRestart 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.

  13. 13
    ChallengeConnect 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.

  14. 14
    RecapA 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.

2

Deliver a result through a callback

9 steps
Build a one-result callback API, add an error path, and see where dependent work belongs.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionGive the caller a next action
  2. 2
    ExampleProduce the delayed value
  3. 3
    ConnectionThe API controls timing
  4. 4
    ChallengeReturn from which function?
  5. 5
    ExampleReport one of two outcomes
  6. 6
    TipStop before reading a failed result
  7. 7
    ChallengeLocate the active error boundary
  8. 8
    ChallengeConnect results and failures across steps
  9. 9
    RecapFollow each outcome to its consumer
3

Build and connect promise outcomes

12 steps
Create a pause promise, attach outcome handlers, and repair a broken dependent chain.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionOne object for a later outcome
  2. 2
    ExampleBuild a reusable pause
  3. 3
    Key InsightKeep state and resolution distinct
  4. 4
    ChallengeCatch a late failure correctly
  5. 5
    ExplanationAttach the right handler
  6. 6
    ExampleTwo subscriptions keep one value
  7. 7
    ChallengeRead a cleanup result
  8. 8
    ExampleBuild the first link
  9. 9
    ExampleMake the next link wait
  10. 10
    ChallengeRepair a detached operation
  11. 11
    ChallengeConnect cleanup to a dependent chain
  12. 12
    RecapCheck the returned path
4

Rewrite a sequence with async and await

12 steps
Turn a callback into an awaitable operation and choose the right loop for multiple items.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionKeep promises, change the reading order
  2. 2
    ExampleAn async call begins immediately
  3. 3
    ChallengeRead the returned value
  4. 4
    ExampleConnect a callback to a promise
  5. 5
    ExampleKeep intermediate values in scope
  6. 6
    ChallengeDoes async alone adapt the result?
  7. 7
    Key InsightTest the value, not its promise
  8. 8
    ExampleforEach does not await callbacks
  9. 9
    TipChoose one loop policy
  10. 10
    ChallengeRepair early completion
  11. 11
    ChallengeConnect adaptation and the loop
  12. 12
    RecapInspect both sides of an async call
5

Keep failure, recovery, and cleanup connected

13 steps
Follow a failed action through catch, add a local fallback, and build a bounded retry policy.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionDecide what a failure should mean
  2. 2
    ExampleWatch a catch recover
  3. 3
    Key InsightLogging chooses a return too
  4. 4
    ChallengeKeep the failed outcome
  5. 5
    ExampleAwait where the catch can see failure
  6. 6
    TipReturn after local handling
  7. 7
    ChallengeChoose the local fallback
  8. 8
    ConnectionEnd the action’s error path
  9. 9
    ExampleKeep context and the original cause
  10. 10
    ExampleRetry a new call, with a limit
  11. 11
    ChallengeStop after success
  12. 12
    ChallengeConnect recovery to cleanup
  13. 13
    RecapGive every outcome a destination
6

Choose what a group of tasks must deliver

11 steps
Overlap independent waits and choose the combinator that matches the required outcome.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionStart with the dependency question
  2. 2
    ExampleStart both operations together
  3. 3
    Key InsightFailure does not cancel the group
  4. 4
    ChallengeRespect one dependency, overlap the rest
  5. 5
    ExplanationKeep every outcome when parts remain useful
  6. 6
    ExampleSeparate first settlement from first success
  7. 7
    ChallengeChoose for a partial page
  8. 8
    TipA timeout ends waiting, not the operation
  9. 9
    ChallengeWhat can settle an empty group?
  10. 10
    ChallengeConnect start timing and failure handling
  11. 11
    RecapChoose the outcome policy

Asynchronous JavaScript practice questions

40 Practice Questions in 4 sets

Set 1: Follow the code from calls to callbacks10 Practice Questions

Question 1 of 10

A page registers a timer, then runs a long synchronous calculation. The timer becomes ready during the calculation. When can its callback run?
  1. As soon as the timer reaches its requested delay.
  2. During the next iteration of the calculation’s loop.
  3. After the calculation gives up the main thread.
  4. On a second thread created by the timer call.
Show answerHide answer
Answer: C
The calculation keeps the main thread until it finishes, so the ready callback must wait. Reaching a timer’s delay cannot interrupt running JavaScript.

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");
  1. "open", "close", "job", "tick"
  2. "open", "tick", "job", "close"
  3. "open", "job", "close", "tick"
  4. "open", "close", "tick", "job"
Show answerHide answer
Answer: A
The current script logs "open" and "close" before any registered callback runs. Its promise handler is a microtask, so it logs "job" before the timer task logs "tick". The timer’s zero delay makes it eligible for later execution; it does not put it ahead of the microtask drain.

Question 3 of 10

A timer has been scheduled as const reminder = setTimeout(showHint, 900);. What statement cancels it before it runs?
  1. clearTimeout(showHint);
  2. setTimeout(reminder, 0);
  3. clearInterval(showHint);
  4. clearTimeout(reminder);
Show answerHide answer
Answer: D
ClearTimeout identifies the timer by its returned handle, reminder. Passing showHint supplies the function, which does not identify this scheduled timer.

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);
}
  1. 2, 3, 4
  2. 5, 5, 5
  3. 2, 2, 2
  4. 3, 4, 5
Show answerHide answer
Answer: B
The callbacks all close over the same var binding. By the time they run, the loop has stopped with index equal to 5, so all three logs read 5. The sequence 2, 3, 4 would occur with a separate let binding per iteration, not with this shared variable.

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);
  1. Three calls have completed.
  2. No calls have completed.
  3. Only one call has completed.
  4. Two calls have completed.
Show answerHide answer
Answer: A
Map invokes all three callbacks before returning, so calls is 3 when logged. Passing a callback does not defer it when its API invokes it synchronously.

Question 6 of 10

What does result contain when the log runs?

function make() {
  setTimeout(() => { return "badge"; }, 0);
}
const result = make();
console.log(result);
  1. The string "badge".
  2. A promise fulfilled with "badge".
  3. undefined
  4. The timer callback function.
Show answerHide answer
Answer: C
Make has no return statement and returns undefined. The later return exits only the timer callback; it cannot supply a value to an outer call that has finished.

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);
  1. err = null;
  2. return;
  3. console.log(item);
  4. callback = null;
Show answerHide answer
Answer: B
Returning leaves this callback after it reports the error, so the item.name line is not reached on failure. Setting err to null only changes the local variable; it does not create a successful result. The error branch must stop normal success processing when no item exists.

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");
  1. "pine", "oak", "elm", "ash", "fir"
  2. "pine", "fir", "oak", "elm", "ash"
  3. "pine", "oak", "fir", "ash", "elm"
  4. "pine", "oak", "fir", "elm", "ash"
Show answerHide answer
Answer: D
The script logs "pine" and initially queues oak before fir. The oak handler adds elm, then its return makes the chained ash handler ready, so both join behind fir in that order. Ash cannot precede elm because elm was queued during the handler before the returned promise fulfilled.

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 });
  1. It calls once; move the success call before the check.
  2. It calls twice; remove the error argument from the first call.
  3. It calls twice; return after reporting the error.
  4. It calls once; add a return after the success call.
Show answerHide answer
Answer: C
The failing condition runs the first callback, but no control-flow statement stops the second call. A return in the failure branch prevents that fall-through and preserves the one-result contract. Returning after the success call is too late because both calls have already happened.

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");
  1. "outside", "executor", "registered", "value"
  2. "outside", "registered", "executor", "value"
  3. "outside", "executor", "value", "registered"
  4. "executor", "outside", "registered", "value"
Show answerHide answer
Answer: A
The Promise constructor runs its executor during construction, between the two surrounding synchronous logs. The then handler runs later as a microtask and logs "value" last. Fulfilling the promise immediately does not make a handler registered with then run synchronously.
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));
  1. 11
  2. undefined
  3. A promise object.
  4. 7
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);
  1. 20
  2. 30
  3. 4
  4. undefined
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);
});
  1. 28
  2. 14
  3. The Error with message "Changed".
  4. undefined
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);
  1. "ready" after about 600 ms.
  2. undefined after about 600 ms.
  3. undefined after about 120 ms.
  4. "ready" after about 480 ms.
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; }
  1. The number 9 directly.
  2. A promise fulfilled with undefined.
  3. A function that will return 9.
  4. A promise fulfilled with 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");
  1. "caller", "inside", "after await"
  2. "inside", "caller", "after await"
  3. "inside", "after await", "caller"
  4. "caller", "after await", "inside"
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A callback API calls done(err, value) once. What should its promise wrapper do when err is an Error?
  1. Call resolve with err and stop that callback.
  2. Throw err after the delayed callback returns.
  3. Call reject with err and stop that callback.
  4. Call resolve with value and then reject with err.
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");
  1. "enter"
  2. "stop"
  3. false
  4. Nothing until the promise settles.
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); });
}
  1. Await the existing forEach call inside the function.
  2. Remove await from record inside the existing async callback.
  3. Use map with async callbacks but ignore the returned array.
  4. Replace forEach with for...of and await record inside the loop.
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);
  1. 42
  2. 13
  3. 90
  4. undefined
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));
  1. "first"
  2. "second 4"
  3. 9
  4. "second 9"
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);
  1. The call to showMessage runs immediately.
  2. The timer waits 500 ms before showMessage.
  3. The timer queues showMessage as a microtask.
  4. A pending promise must settle first.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

An async wrapper starts an error-first callback operation but never returns a promise connected to its callback. Its body reaches the end immediately. What does awaiting the wrapper yield?
  1. The callback’s eventual successful value.
  2. The callback’s eventual Error object.
  3. Undefined before the callback result is ready.
  4. A wait that lasts until the callback is called.
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));
  1. Outer rejects with "replacement" immediately.
  2. Outer follows inner’s rejection reason.
  3. Outer fulfills with the inner promise object.
  4. Outer remains pending after inner rejects.
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");
  1. "a", "f", "e", "b", "c", "d"
  2. "a", "f", "b", "c", "e", "d"
  3. "a", "b", "f", "e", "c", "d"
  4. "a", "f", "b", "e", "c", "d"
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

An account call takes about 200 ms. After it fulfills, two independent account-based calls take about 500 ms and 800 ms. They both need that account and all results are required. What plan gives the earliest complete result?
  1. Await the account, then start both later calls with Promise.all; about 1000 ms.
  2. Start all three in Promise.all immediately; about 800 ms.
  3. Await the account, then await each later call in turn; about 1500 ms.
  4. Await the account, then race the later calls; about 700 ms.
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; }
  }
}
  1. It catches the rejection but exits because the loop starts at zero.
  2. It cannot retry because task is passed as a function instead of a promise.
  3. It retries all three times before any returned promise can settle.
  4. It returns the promise before its rejection can reach the local catch.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

What is the state of Promise.race([]) when no inputs will ever be added?
  1. It fulfills with [].
  2. It rejects right away.
  3. It remains pending.
  4. It resolves to undefined.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

Two attempts start together. The second input rejects first; the first input rejects later. What does Promise.any([first, second]) eventually provide to catch?
  1. The second input’s Error as soon as it rejects.
  2. An AggregateError with reasons ordered first, then second.
  3. An AggregateError with reasons ordered second, then first.
  4. The first input’s Error alone after the later rejection.
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");
  1. "cleanup", "caller", "fallback"
  2. "caller", "fallback", "cleanup"
  3. "caller", "cleanup", "lost"
  4. "caller", "cleanup", "fallback"
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

A function wraps an error as new Error("Report failed", { cause: original }). What property keeps the original Error available?
  1. message
  2. name
  3. cause
  4. status
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));
  1. "missing"
  2. "local"
  3. undefined
  4. Nothing because the function stays pending.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A catch handler returns "default" after receiving a rejection. What does the following then handler receive?
  1. The original rejection reason.
  2. An undefined value despite the return.
  3. A rejected promise containing "default".
  4. The fulfilled value "default".
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));
  1. "same call" only.
  2. "later" only.
  3. "same call", then "later".
  4. "start", then "later".
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();
  1. "saved", "reset"
  2. "reset", "busy"
  3. "busy", "reset"
  4. "busy", "saved", "reset"
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A retry helper awaits a fresh task in try, returns on success, and retries in catch until its limit of four calls. The task rejects on calls 1 and 2, then fulfills with "ok" on call 3. What happens?
  1. It makes four calls and fulfills with "ok".
  2. It makes one call and rejects with the first error.
  3. It makes three calls and rejects with the second error.
  4. It makes three calls and fulfills with "ok".
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

Two independent timer-based functions take about 350 ms and 900 ms. Both are called inside the input array to await Promise.all(...) and both succeed. About how long does the combined wait take?
  1. About 900 ms.
  2. About 1250 ms.
  3. About 350 ms.
  4. About 550 ms.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Tasks are started together: A fulfills after 800 ms, B rejects after 250 ms, and C logs a side effect and fulfills after 600 ms. What does Promise.all([A, B, C]) do?
  1. Reject near 800 ms; C can still log near 600 ms.
  2. Reject near 250 ms; C can still log near 600 ms.
  3. Reject near 250 ms; C is canceled before its log.
  4. Fulfill near 800 ms with A and C but no B result.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

Independent panels must each show either their result or their own error after all attempts finish. What promise combinator supplies the needed records?
  1. Promise.allSettled
  2. Promise.all
  3. Promise.race
  4. Promise.any
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Two promises begin together: one rejects after about 150 ms and one fulfills with "backup" after about 400 ms. How do race and any differ?
  1. Race fulfills near 400 ms; any rejects near 150 ms.
  2. Both reject near 150 ms with the first failure.
  3. Race rejects near 150 ms; any fulfills near 400 ms.
  4. Both fulfill near 400 ms with the backup value.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

72 Key Terms, each with its definition

A

AggregateError
An AggregateError groups multiple errors; Promise.any uses it with an input-ordered errors array when all attempts fail.
When two attempts fail in reverse order, the errors array still lists their reasons in the original input order.
Async function
An async function always returns a promise for its returned outcome or thrown error.
Calling an async function that returns a number gives its caller a promise, even without an await in its body.
Asynchronous callback
An asynchronous callback is invoked later through scheduling, after the operation that registered it has returned to its caller.
A label-preparation function returns first; its result callback later receives the completed label.
Asynchronous operation
An asynchronous operation starts now and delivers its outcome later while other code can continue.
A timer is registered now, while its callback logs a message after the environment has managed the delay.
Await
Await pauses its async function until an outcome is available, supplying a fulfilled value or throwing the rejection reason at that line.
Awaiting a pause leaves the caller free to log a message before the paused function resumes its next statement.

B

Blocking
Blocking occupies a thread with work, keeping other scheduled code from running on it.
A busy loop can delay a zero-delay timer because the callback still needs the occupied main thread.

C

Call stack
The call stack records active function calls, with the current call at the top.
If a handler calls a helper, the helper runs at the top of the stack until it returns to that handler.
Callback
A callback is a function passed to another function for that function to invoke.
Passing a named handler supplies an action; adding parentheses would call it while evaluating the argument.
Callback hell
Callback hell is deeply nested callback code in which each dependent step adds another level and often another error-handling branch.
Reading a profile, then projects, then tasks creates several nested callbacks that are hard to scan.
Catch boundary
A catch boundary is the connected work whose failures can reach a particular catch handler, determined by placement and awaited calls.
A catch in the middle handles earlier steps but cannot handle a new error thrown by a later handler.
Concurrency
Concurrency allows independent operations to remain in progress during overlapping waits, while their JavaScript handlers still execute one at a time.
Two timers begin together, so the caller waits about as long as the slower timer instead of adding both delays.
Continuation
A continuation is the code in an async function that resumes after an await.
Even awaiting a plain number defers the remaining lines instead of running them before the caller continues.

D

Debouncing
Debouncing restarts a timer on each event so an action waits for a quiet period.
Typing several characters quickly replaces the pending timer each time, leaving one update after typing stops.
Dependent operation
A dependent operation needs an earlier operation’s result and must start only when that required input has become available.
A callback receives a user, then starts the next operation with that user as its required argument.

E

Early return
An early return leaves a function before its remaining statements run.
Returning after logging a callback error prevents the next line from reading a missing result’s name.
Error cause
An error cause is an original failure retained in a new Error’s cause property.
An outer error that says "Report failed" still holds, in its cause, the device error that prevented the report’s preparation.
Error event
An error event reports failure through an event handler, allowing a browser API to use a separate failure path from its success event.
An image has its failure handler assigned before its source starts loading, alongside its success handler.
Error object
An Error object represents a failure with a message and name, and typically includes stack information useful for locating the problem.
Reading the message of a caught Error made with the message "Missing label" gives that text without requiring the user interface to show its stack.
Error-first callback
An error-first callback receives an error argument before its result, using null for success and an Error object for failure.
The caller checks the error before reading the item, so a failed operation does not trigger success processing.
Event loop
The event loop coordinates tasks, microtask checkpoints, and rendering opportunities so that ready work can run in an orderly way.
Tracing a script first and its microtasks next explains why a script’s promise message appears before its timer message.
Executor
An executor is the function the Promise constructor calls immediately with resolve and reject.
The executor registers a timer immediately; the success handler still runs later through the microtask queue.

F

Fail-fast behavior
Fail-fast behavior rejects a combined wait as soon as a required input fails, without waiting for the remaining inputs to finish.
A grouped preparation fails when one part fails after 200 ms, although another part is still working toward its later completion.
Fallback
A fallback is an alternative value or behavior used after failure when the larger action can still do useful work without its original result.
A missing optional theme setting falls back to a light theme while the rest of the page keeps loading.
Finally block
A finally block runs as control leaves its try/catch structure, providing cleanup after success, failure, or a return statement.
A button is enabled in finally so both successful and failed attempts leave the interface ready for another action.
Finally callback
A finally callback runs after either promise outcome, receiving no argument and normally preserving the earlier value or reason.
The callback hides a spinner after success or failure; a throw during that cleanup would become a new failure.
Flat chain
A flat chain expresses dependent promise steps as successive outer-level handlers, keeping their timing and error propagation connected.
Returning each new operation lets one catch at the end receive failures from the connected steps above it.
Forgotten await
A forgotten await leaves a promise in a place where code expects its eventual value, causing incorrect conditions or numeric operations.
Multiplying the promise returned by a quantity function produces NaN instead of multiplying its future number.
Fulfilled
Fulfilled is the settled promise state holding a successful value.
A successful quantity check fulfills with the remaining count, and its success handler receives that number.
Fulfillment handler
A fulfillment handler is a function registered with then that receives a promise’s successful value and determines a new promise’s outcome.
A handler multiplies the received count and returns the new count for the next link in the chain.

H

Host environment
The host environment is the browser or Node.js runtime that provides facilities such as timers and manages their scheduling.
The browser tracks a timer’s delay, then makes its callback available for JavaScript to run later.

I

Ignored callback promise
An ignored callback promise is a promise returned by a callback that its calling API does not use to track completion or failure.
The forEach method ignores an async callback’s promise, so the surrounding function can finish before that work.
Independent operations
Independent operations can start without one another’s results, allowing their waiting times to overlap when the program starts them together.
Preparing a cover and a label can overlap when both already have all the input data they need.
Input order
Input order is the arrangement of values or promises supplied to a combinator.
Destructuring the Promise.all result names values according to the original array’s positions.
Intermediate result
An intermediate result is a value produced during a sequence and used by a later step.
An async function stores the user and then the orders, keeping both local values available for the final message.
Interval
An interval repeatedly schedules a timer callback until it is cleared.
A counter clears its interval after the fifth tick so the page stops producing additional tick messages.

M

Main thread
The main thread runs the page’s ordinary JavaScript and screen-related work.
A long calculation on the main thread prevents a ready click handler from running until the calculation finishes.
Microtask
A microtask is a queued job, such as a promise handler, run at a microtask checkpoint before the event loop starts another task.
A fulfilled promise’s handler runs after the current script but before a ready zero-delay timer task.
Microtask starvation
Microtask starvation delays tasks and rendering because the microtask queue never finishes draining.
A microtask that always queues another copy of itself can prevent a click handler from ever getting a turn.
Missing return
A missing return leaves a handler’s result undefined and can detach work it was meant to return.
A braced handler starts a pause but forgets to return it, so the next handler runs before that pause ends.

N

Nested timeout
A nested timeout schedules another timeout from a timer callback, allowing the next delay to begin after the current run’s work.
A progress check schedules its next timeout only after its current synchronous check has finished.

P

Pending
Pending is the promise state before fulfillment or rejection.
A timer-backed pause stays pending while the environment waits to invoke the function that resolves it.
Promise
A promise is an object representing one eventual outcome, which can supply a value or a failure reason to connected handlers.
A function returns a promise immediately, then a timer later supplies the value its caller is waiting for.
Promise adoption
Promise adoption makes one promise follow another promise’s eventual fulfillment value or rejection reason instead of treating it as a plain object.
Returning a promise from then makes the next handler wait and receive its value, without an extra nested promise.
Promise chain
A promise chain connects handlers through returned promises, passing each returned value or adopted asynchronous outcome to the next link.
One handler returns a preparation promise so the next handler receives the completed item after preparation finishes.
Promise subscription
A promise subscription registers a handler on a specific promise.
Two handlers on one fulfilled promise each get its original number, even when one returns a changed number.
Promise wrapper
A promise wrapper exposes another API’s outcome through a returned promise, letting callers consume the result with handlers or await.
Wrapping a timer creates a reusable pause whose completion is connected to the promise returned to the caller.
Promise.all
Promise.all combines input promises into one promise for all successful values in input order, rejecting on the first input rejection.
The slower first input remains first in the result array even when the second input finishes earlier.
Promise.allSettled
Promise.allSettled waits for every input and fulfills with input-ordered records describing each fulfillment value or rejection reason.
A page keeps a successful panel and displays an error for a failed panel after inspecting both outcome records.
Promise.any
Promise.any fulfills with the first successful input value and rejects with AggregateError only when every input has rejected.
An early failed attempt is ignored while another attempt still has a chance to provide a usable result.
Promise.race
Promise.race follows the first input to settle, taking either its successful value or its rejection reason as the combined outcome.
A rejecting timeout wins against a slower successful operation, while the slow operation continues running.
Promisification
Promisification adapts a callback API into a promise-returning API by connecting the callback’s success and failure to settlement.
A wrapper around a label callback API resolves with the label or rejects with the error so an async caller can await the completed operation.

R

Recovery
Recovery changes a failed promise path into a successful one by returning a usable value or fulfilled promise from a rejection handler.
Returning a default label in catch gives the next success handler a value it can use to continue.
Rejected
Rejected is the settled promise state holding a failure reason.
A failed stock check rejects with an Error whose message explains why the requested quantity is unavailable.
Rejection handler
A rejection handler receives a promise’s failure reason and can recover with a value or preserve failure by throwing an error.
A catch handler supplies default settings when a failed settings operation should not stop the larger action.
Rejection propagation
Rejection propagation carries failure through connected promise links, skipping fulfillment handlers until a rejection handler deals with it.
An error thrown in the second step reaches the final catch without running the third step’s success handler.
Resolved
A resolved promise has its outcome locked in, possibly by adopting another promise that is still pending and may later reject.
Resolving a wrapper with a pending promise makes the wrapper follow that promise’s later success or failure.
Rethrow
To rethrow is to throw a caught error again so a later handler can receive it.
A lower-level catch logs an error and rethrows it, leaving the outer action responsible for the user’s message.
Retry
A retry starts a fresh attempt after failure, usually with a delay and a finite attempt limit when the failure may be temporary.
A task succeeds on its third call, so a helper with four available attempts returns without making a fourth call.
Run to completion
Run to completion means that once a task or microtask starts, asynchronously queued callbacks cannot interrupt its running code.
A timer becoming ready halfway through a loop does not insert its callback between the loop’s statements.

S

Sequential processing
Sequential processing starts each asynchronous step after the previous step completes, preserving dependencies or a required operation order.
A for...of loop awaits one record operation before starting the next, and logs completion after the last success.
Settled
A settled promise has fulfilled or rejected, fixing its state and outcome.
Calling resolve again after a promise has fulfilled does not replace the value received by its handlers.
Swallowed error
A swallowed error is a caught failure that is hidden without useful reporting or propagation, making the broken action harder to diagnose.
An empty catch around a save operation leaves the page silent even though the save did not complete successfully.
Synchronous callback
A synchronous callback is invoked before the receiving function returns, so its work happens during that function’s current call.
The callback passed to map doubles each element before the completed result array is returned.
Synchronous code
Synchronous code finishes each statement before moving to the next.
A long loop delays the next log because each iteration executes before control can leave the loop.

T

Task
A task is a scheduled unit of work, such as a script, event handler, or timer callback.
The browser finishes the current script before it starts a timer callback that has become ready.
Throw
A throw interrupts normal execution at that point and transfers an error to an applicable catch, or propagates it outward if none applies.
A rejected awaited promise behaves as a throw at its await line, skipping the remaining statements in try.
Timeout
A timeout is a scheduled one-time callback that becomes eligible after a delay, with actual execution also depending on scheduling.
A hint scheduled for 600 ms may appear later if the main thread is still doing other work at that time.
Timer handle
A timer handle identifies a scheduled timer so the program can cancel it.
Saving the returned handle lets the next input event cancel the earlier pending suggestion update.
Top-level await
Top-level await is await used directly in a JavaScript module’s body, where the language permits it outside an async function.
A module can await its initialization directly, while a normal function still needs its own async keyword.
Truthy value
A truthy value is treated as true in a condition; a promise is truthy because it is an object, regardless of its eventual result.
A promise that fulfills with false still enters an if branch when the code tests the promise object directly.
Try block
A try block protects statements whose throws can transfer control to its associated catch.
Awaiting a preparation operation inside try lets the surrounding function recover when that operation rejects later.

U

Unhandled rejection
An unhandled rejection is a promise rejection that still lacks a rejection handler when the host performs its unhandled-rejection check.
A background action started without catch may reject after its caller has continued, leaving its failure unhandled.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started