Course outline · 0% complete

0/27 lessons0%

Course overview →

async/await basics

lesson 6-1 · ~12 min · 14/27

The next .then receives "x", because the chain waits for a returned promise and passes its value on.

Returning a promise makes the chain wait and adopt its result rather than handing the promise object downstream.

async/await, today's topic, is syntax for exactly that behavior. The waiting and unwrapping are identical, and only the way you write them changes.

Holding that equivalence in mind pays off twice. It explains why await gives you a value instead of a promise, and it explains why an async function's return value gets wrapped in one.

Everything from unit 5 still applies underneath, including the single settlement, the microtask scheduling, and the falling-through of errors.

await is chain syntax

Two keywords rewrite promise chains as ordinary-looking lines.

  • async before a function makes it always return a promise, so return 5 inside it fulfills that promise with 5.
  • await promise, usable inside async functions and at the top level of a module, pauses the function until the promise settles and then hands you the unwrapped value.

Here is the crucial nuance. await pauses that one function, not the program.

The rest of your script keeps running, and the paused function resumes later as a microtask, which is the mechanism from lesson 4-2.

The syntax exists because .then chains still read inside-out once conditions, loops, or retries get involved. Try writing "attempt three times, then fall back" as a chain and the awkwardness is immediate.

With await, async code uses the same if, for, and try tools as everything else. A retry is a loop, a conditional fetch is an if, and a cleanup step is a finally.

That is why it is the default style in modern codebases and the one interviewers expect. The chain form is worth reading fluently, and it is no longer worth writing by hand for sequential work.

Where an awaiting function pauses

The script does not wait along with the function.

function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function main() {
  console.log("fetching...");
  const user = await delay(30, "Ada");
  console.log("got " + user);
}

main();
console.log("script continues while main is paused");

Output

fetching...
script continues while main is paused
got Ada

main pauses at the await, the script continues past the call, and the awaited value arrives last.

"fetching..." prints synchronously, before main() even returns, since everything up to the first await runs on the current stack.

main() returned a pending promise that nothing here uses. Adding .then(() => console.log("done")) to it would print after got Ada.

user is the string "Ada" rather than a promise, which is the unwrapping half of await doing its work.

The 30 ms wait blocks nothing. A timer, a click handler, or another async function would all run normally during it, which is the difference from the busy-wait in lesson 4-1.

Errors: plain try/catch

With await, a rejected promise throws at the await line.

So error handling is the try/catch you already know, instead of the .catch chains from lesson 5-3.

async function load() {
  try {
    const user = await fetchUser(42);
    console.log(user);
  } catch (err) {
    console.log("fallback: guest");
  }
}

Same rejection, same recovery, straighter code. This is the style most codebases and interviewers expect from you by default.

One try block can cover several awaits, exactly as one trailing .catch covered a whole chain. The tradeoff is the same too, since the block stops at the first failure.

finally works here as well, and it runs whether the awaits succeeded or threw, which makes it the right place to close a connection or hide a spinner.

The trap is an await outside the try. Only awaits inside the block are protected, and a rejected promise awaited after it escapes as an unhandled rejection.

A rejection surfacing as a throw

The same fetchUser as lesson 5-3, called twice.

function fetchUser(id) {
  return new Promise((resolve, reject) =>
    setTimeout(() => (id === 1 ? resolve("Ada") : reject(new Error("unknown user"))), 10)
  );
}

async function load(id) {
  try {
    const name = await fetchUser(id);
    console.log("hello " + name);
  } catch {
    console.log("fallback: guest");
  }
}

load(1).then(() => load(42));

Output

hello Ada
fallback: guest

Id 1 resolves and prints normally. Id 42 rejects, the rejection surfaces as a thrown error at the await line, and the catch recovers.

catch with no parameter is legal modern syntax, and it says the reason is deliberately unused. Writing catch (err) and then ignoring err says the same thing less clearly.

load returns a promise even though it never returns a value, which is what makes load(1).then(...) work. That promise fulfills with undefined once the function body finishes.

The .then is what sequences the two calls here. Calling load(1) and load(42) on separate lines would start both at once, and the output order would depend on the timers.

Because the catch handles the failure, load(42)'s promise fulfills rather than rejecting. Catching an error converts it into a normal result, which is easy to forget when a caller needs to know something went wrong.

Calling it gives you a promise that fulfills with 7.

Async functions always return a promise, wrapping whatever you return, and there is no way to opt out.

To get the 7 you await the call or attach .then, so const n = await f() gives the number and const n = f() gives the promise.

That trips people up when they forget the await and try to use the result as a number. f() + 1 produces the string "[object Promise]1" rather than 8, which is a confusing symptom for a simple mistake.

The wrapping is idempotent in a useful way. Returning a promise from an async function does not give you a promise of a promise, since it is adopted just as in a .then.

Throwing instead of returning produces a rejected promise, which is the mirror image of the same rule. An async function never throws synchronously to its caller.

Rewriting a chain with await

The chain version was fetchUser().then((name) => console.log("hello " + name.toUpperCase())).

function fetchUser() {
  return new Promise((resolve) => setTimeout(() => resolve("Ada"), 20));
}

async function main() {
  const name = await fetchUser();
  console.log("hello " + name.toUpperCase());
}

main();

Output

hello ADA

The whole rewrite is one await line followed by a normal console.log, with no .then anywhere.

name is a plain string inside main, so every string method is available directly without a callback parameter.

The timing is unchanged from the chain version. Both print after roughly 20 ms, and both leave the surrounding script free to run.

Adding a second step shows why this style wins. Fetching the user's orders next is another await line at the same indentation, while the chain needs another .then and a returned promise.

main() is called without awaiting anything, which is fine at the top of a script and worth flagging in general. That returned promise has no .catch, so a rejection inside main would go unhandled.