Async/await

Promises give you a solid model for asynchronous work, but chaining .then() calls can get noisy fast. async/await is a thinner syntax layered on top of promises. It lets you write code that looks sequential while still running asynchronously underneath. Two keywords do all the work, and once they click you rarely reach for .then again.

Async functions

Start with async. You put it in front of a function declaration:

async function f() {
  return 1;
}

One rule covers what it does: an async function always returns a promise. If your code returns a plain value, JavaScript wraps it in a resolved promise for you.

So this f() hands back a promise that resolves to 1. You can confirm it by attaching .then:

async function f() {
  return 1;
}

f().then(alert); // 1

Returning the promise by hand gives the same result — the wrapping just happens automatically when you don’t:

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1
return 1
Promise<resolved: 1>
return Promise.resolve(1)
Promise<resolved: 1>
async wraps whatever the function returns in a resolved promise. A returned promise passes through unchanged.

See it for yourself. The button calls getValue() and inspects what comes back — a promise, never the bare number. Click unwrap it to attach .then and pull the real value out.

interactiveAn async function always returns a promise

That’s the first half. The async keyword also unlocks a second one, await, which only works inside async functions.

Await

The syntax is a single expression:

// works only inside async functions
let value = await promise;

await tells JavaScript to pause the function until that promise settles, then hand back its result as the value.

Here’s a promise that resolves after one second:

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // wait until the promise resolves (*)

  alert(result); // "done!"
}

f();

At line (*) the function pauses. A second later the promise resolves, result becomes "done!", and execution continues to the alert.

Read that carefully: await suspends the function, not the whole engine. While your function is parked waiting, JavaScript is free to run other things — other event handlers, other scripts, rendering. No CPU is burned spinning in place. The engine simply notes where to resume and moves on to whatever else is queued.

let result = await promise
⏸ function pauses here
ENGINE (meanwhile)
runs other events
handles clicks, timers…
PROMISE (1 sec later)
settles → “done!”
▶ function resumes
alert(result) → “done!”
await parks the async function and returns control to the engine. When the promise settles, the function resumes right where it left off.

Functionally, await promise is a cleaner way of writing promise.then(result => …). The payoff is that the result lands in a plain variable, and the lines below read like ordinary sequential code.

Run the next demo to watch that “pause, then resume” play out. Each await waits about 600ms for a timer to resolve before the next line runs — notice the elapsed-time stamps climb in order, one step after the other.

interactiveawait pauses, then resumes with the value

Rewriting a promise chain

To feel the difference, take the showBadge() example from Promises chaining and convert it. Two mechanical changes do the job:

  1. Swap each .then for an await.
  2. Mark the function async so await is allowed.
async function showBadge() {

  // read our JSON
  let response = await fetch('/data/members/member.json');
  let member = await response.json();

  // read the member's profile from the API
  let apiResponse = await fetch(`https://api.example.com/members/${member.handle}`);
  let profile = await apiResponse.json();

  // show the badge
  let img = document.createElement('img');
  img.src = profile.badge_url;
  img.className = "member-badge-example";
  document.body.append(img);

  // wait 3 seconds
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return profile;
}

showBadge();

The nested callbacks and chained .then handlers collapse into a flat list of statements. Each await reads like “get this, then keep going,” which is much closer to how you’d describe the steps out loud.

Error handling

A promise has two ways to settle. When it resolves, await promise gives you the value. When it rejects, await throws the rejection reason — as if a throw sat on that exact line.

These two functions behave identically:

async function f() {
  await Promise.reject(new Error("Boom!"));
}
async function f() {
  throw new Error("Boom!");
}

The difference in practice is timing. A real promise usually rejects after some delay (a failed network request, say), so await throws only once the rejection actually arrives — not instantly.

Because a rejected await throws, you catch it with ordinary try..catch:

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

When the error fires, control jumps straight to catch. One try block can guard several awaits at once, so any of them failing lands in the same handler:

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // catches errors from both fetch and response.json
    alert(err);
  }
}

f();
try
await fetch(…) ← rejects
await response.json() (skipped)
↓ thrown error
catch (err)
alert(err)
A rejected await throws into the surrounding try..catch, skipping the rest of the try block.

Try both outcomes below. The same try..catch block wraps one await: when the promise resolves you land in the try, and when it rejects the thrown error jumps straight to catch — no special promise syntax needed.

interactiveA rejected await throws into try..catch

If you skip try..catch, the error doesn’t vanish — the promise that f() returned becomes rejected instead. You can attach .catch to handle it from the outside:

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)

Forget the .catch and the rejection goes unhandled, showing up as an error in the console. As covered in Error handling with promises, you can catch these globally with an unhandledrejection event handler.

Summary

Two keywords, and here’s what each buys you.

async before a function does two things:

  1. Forces the function to return a promise, wrapping plain return values automatically.
  2. Permits await inside its body.

await before a promise pauses the function until that promise settles, then:

  1. On rejection, it throws — identical to throw error at that line, catchable with try..catch.
  2. On resolution, it returns the value.
RESOLVED
let x = await p
→ x becomes the value
REJECTED
let x = await p
→ throws at this line
The two behaviors of await once its promise settles.

Together they let you write asynchronous code that reads top to bottom. You’ll seldom need promise.then/catch directly, but they haven’t gone anywhere — async/await is built on promises, and in places like the outermost scope you’ll still reach for them. And Promise.all remains the tool of choice when you want many tasks running in parallel.