Asynchronous JavaScript · Lesson 51 of 77 · concept
async and await
About 65 minutes.
By the end of this lesson you can
- Explain what `async` does to a function's return value, including when the body throws.
- Explain what `await` pauses — and, more importantly, what it does not pause.
- Rewrite a short `.then` chain as `async`/`await`, and back again.
- Handle a rejected promise with `try` / `catch` / `finally` around an `await`.
- Say where `await` is allowed, and why top-level `await` fails in this sandbox.
Read, not run. The sandbox on this site has no promise callbacks (nothing after a .then runs), top-level await, so the examples in this lesson are shown rather than executed. Where an example cannot run, it says so and explains why instead of leaving you to find out.
Same machinery, different spelling
Explanation
`async` and `await` are not a third asynchronous system sitting beside callbacks and promises. They are a way of writing promise code that reads like ordinary top-to-bottom code. Underneath, the promises and the microtask queue from the last two lessons are doing exactly what they did before.
That is good news twice over. Everything you learned about states, settling once and chaining still applies. And when something goes wrong, the debugging question is still "which promise is this, and what settled it?"
There are only two pieces to learn: what `async` does to a function, and what `await` does inside one.
async: the return value is always a promise
Explanation
Put `async` in front of a function and one thing changes: the value you return is no longer what the caller gets. MDN is blunt about it — async functions always return a promise, and a returned value that is not already a promise is wrapped in one.
So `async function greet() { return "hi"; }` called as `greet()` hands back a promise that fulfils with "hi". The string is in there; it is not what you are holding.
Throwing follows the same route. An exception that escapes the body does not reach the caller as an exception — it becomes a rejection of the returned promise. This is the trade that makes the rest work: because both outcomes travel inside one promise, the caller has one thing to wait on and one place to look for failure.
await: pauses this function, not the program
Explanation
`await somePromise` waits for the promise and gives you its fulfilment value — as a plain value, on that line, in a variable. That is the whole appeal: the pyramid of handlers becomes a straight line.
But read the word "waits" carefully. Nothing about `await` blocks the thread. What MDN describes is that the awaited expression is evaluated, the code depending on it is paused, control leaves the function and returns to the caller, and when the value resolves a microtask is scheduled to continue the paused code.
So the caller carries on immediately. The rest of your async function is a continuation, queued like any other job. `await` is a promise handler wearing more comfortable clothes.
One detail worth knowing before it confuses you: the body up to and including the first `await` runs synchronously, so an async function with no `await` in it runs entirely synchronously — it just hands back a promise at the end. And awaiting a non-promise still pauses: MDN notes the value is converted to a resolved promise and execution still waits until the next tick.
Cut the function at every await
A mental model
Here is the model to keep. Take an async function and draw a line at each `await`. Everything above the first line runs when you call it, synchronously, like any function. Every piece below a line is a separate job, queued when the awaited promise settles.
Now the surprising behaviours stop being surprising. Code after the call runs before the code after the first `await`, because the caller resumed while your function was parked. A `console.log` at the top of an async function prints immediately, while one after an `await` does not. And a variable you set after an `await` is not set yet for anybody who reads it in between.
Use this model to answer ordering questions the same mechanical way as the last lesson: where are the cut lines, and which piece is queued when?
Errors become ordinary exceptions again
Explanation
When an awaited promise rejects, the `await` expression throws the rejection reason. That is the second big win after flat code: `try` / `catch` / `finally`, which you already learned for synchronous code, works unchanged.
So the three shapes converge. An error-first callback checks a parameter, a promise chain uses `.catch`, and an async function uses `try`/`catch` — and the last of those is the one you would have written before you knew anything about asynchronous code at all.
Two things do not change. An unhandled rejection is still unhandled: if nothing catches it, it propagates out and rejects the promise your async function returned, and if nobody handles that either the runtime reports it. And `finally` still means "either way" — the natural place for cleanup.
Five await mistakes
A common mistake
Forgetting `await`. `const user = loadUser();` gives you a promise; `user.name` is `undefined` and nothing warns you. The whole final lesson of this module is about spotting this symptom.
Awaiting outside an async function. `await` is allowed inside an async function or at the top level of a module. In an ordinary function it is a syntax error, and in this sandbox — which evaluates programs as a script, not a module — top-level `await` is a syntax error too. Wrap the work in an async function and call it.
Assuming `await` blocks everything. It suspends only the function it appears in. Timers keep firing, event handlers keep running, and the caller has already continued.
`return promise` inside a `try`. A plain `return somePromise;` hands the promise back before this function's `catch` can see it fail, so a rejection escapes to the caller instead. `return await somePromise;` waits here, turning the rejection into an exception this `try` block can actually catch.
Sprinkling `async` everywhere. `async` on a function with no `await` in it adds a promise wrapper and nothing else, forcing every caller to await something that was never asynchronous. Add it when you need `await` inside.
Why this is the default style now
Why it matters
The gain is not that there is less to type. It is that asynchronous code goes back to using the tools you already have: variables hold values, `try`/`catch` handles failure, `if` and `for` work normally, and the order of the lines matches the order of the steps.
That said, `.then` has not gone anywhere and you will read plenty of it. The two mix freely, because an async function returns a promise and `await` accepts one. The rule of thumb is one style per function: mixing `await` and `.then` inside a single function is where error handling gets genuinely confusing, and that is the last mistake in the next lesson.
Recap
Recap
`async` makes a function return a promise — fulfilled with what you return, rejected with what you throw. `await` unwraps a promise into a value, pausing only that function while the caller carries on; the rest of the body becomes a queued continuation. Rejections become exceptions, so `try`/`catch`/`finally` works. `await` needs an async function (or a module's top level), and inside a `try` you often want `return await`.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
What `async` does to the return value
One `async` keyword, and the return type changes for every caller. This runs in the sandbox because nothing is awaited — the function body has no `await`, so it completes synchronously and simply hands back a promise.
1async function greet(name) {2 return "Hello, " + name;3}4 5const result = greet("Ada");6 7console.log(typeof result);8console.log(result instanceof Promise);9console.log(result === "Hello, Ada");10console.log(String(result));11 - line 2
The body returns a string. The caller does not get a string, because `async` wraps it — MDN: async functions always return a promise.
- line 9
`false`. The greeting exists and is already inside the promise; it just is not what `result` holds.
- line 10
`[object Promise]`. Recognise this in real output: it is what a forgotten `await` looks like once it reaches a string.
What it prints
objecttruefalse[object Promise]
Where the cut lines fall
The numbered lines are the real execution order in a browser or Node. Read them in numeric order and notice that line 3 is in the caller, sandwiched between two lines of the async function.
1async function work() {2 console.log("2 — inside work, before any await");3 const value = await Promise.resolve(10);4 console.log("4 — resumed as a microtask, value is " + value);5 return value * 2;6}7 8console.log("1 — before calling work");9const pending = work();10console.log("3 — work() already returned; it is not finished");11 12pending.then(function (doubled) {13 console.log("5 — the returned promise fulfilled with " + doubled);14});15 - line 2
Runs synchronously during the call. Everything up to and including the first `await` does — MDN says so explicitly.
- line 3
The cut line. Control leaves `work` here and goes back to the caller; the rest of the body is queued for when the promise settles.
- line 9
`pending` is a promise, not 20. `work` is parked at line 3 at this moment.
- line 10
Prints third, before line 4. This is the single most useful thing to internalise: `await` did not stop the caller.
This one does not run hereThe sandbox never drains the promise job queue, so nothing after the `await` — lines 4 and 5 — would ever run here. It would print 1, 2, 3 and then stop, which would teach the wrong lesson.
Failure handling goes back to normal
The same three outcomes as the error-first callback in lesson 1, written with the syntax you already knew before this module started. Compare the shapes: the logic is identical, the ceremony is gone.
1async function showReport(id) {2 startSpinner();3 try {4 const report = await loadReport(id);5 console.log("rows: " + report.rows);6 return report;7 } catch (error) {8 console.log("could not load report " + id + ": " + error.message);9 return null;10 } finally {11 stopSpinner();12 }13}14 - line 4
If `loadReport` rejects, this line throws the rejection reason — so an asynchronous failure is caught by an ordinary `catch`.
- line 6
`return report;` inside a `try` is fine here because the value is already unwrapped. Returning a PROMISE from a try block is the case that needs `return await` instead.
- line 10
`finally` runs on both paths, exactly as it does in synchronous code. The spinner stops whatever happened.
This one does not run here`loadReport` is an asynchronous stand-in with no counterpart here, and the sandbox cannot resume the function after the `await` without a job-queue drain.
The same three steps, twice
One chain, one async function, identical behaviour. Read them side by side and note what disappears: the handler functions, the nesting of the second step inside the first, and the separate `.catch`.
1// With .then2function summariseChained(id) {3 return loadUser(id)4 .then(function (user) {5 return loadOrders(user.id);6 })7 .then(function (orders) {8 return orders.length + " orders";9 })10 .catch(function (error) {11 return "failed: " + error.message;12 });13}14 15// With async / await16async function summarise(id) {17 try {18 const user = await loadUser(id);19 const orders = await loadOrders(user.id);20 return orders.length + " orders";21 } catch (error) {22 return "failed: " + error.message;23 }24}25 - line 5
`return loadOrders(...)` — returning the promise is what makes the next link wait. Forget it and the chain moves on with `undefined`.
- line 19
The `await` version needs no equivalent of that `return`: `user` is a plain object and `orders` is a plain array, both in scope on the following lines.
- line 16
Both functions return a promise. `summarise` is `async`, so its return value is wrapped automatically; the chained version returns the chain's own promise.
This one does not run here`loadUser` and `loadOrders` are asynchronous stand-ins, and neither version could progress past its first suspension point without a job-queue drain.
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
`ask` is an async function whose body returns the number 5. What do these three lines print?
async function ask() { return 5;}const answer = ask();console.log(typeof answer);console.log(answer === 5);console.log(answer instanceof Promise);
While an async function is parked at an `await`, what else is stopped?
`await loadUser(3)` and `loadUser` rejects with `new Error("no such user")`. What happens?
Why does a bare `const value = await Promise.resolve(1);` at the top of a run-pad snippet fail with a SyntaxError?
No exercise in this lesson
Nothing after an `await` can run here: the sandbox never drains the promise job queue (spec item RP-1), and top-level `await` is a syntax error besides. Any test would have to grade a function that stops at its first suspension point.
Worth remembering
- What does `async` change about a function?
Its return value. The caller always receives a promise — fulfilled with whatever the body returns, rejected with whatever the body throws. Nothing else about the function changes, and a body with no `await` still runs synchronously.
- What does `await` pause?
Only the async function it appears in. Control returns to the caller immediately and the rest of the body is queued as a microtask for when the promise settles. The thread is never blocked.
- Inside a `try` block, why write `return await work();` rather than `return work();`?
A plain `return` hands the promise to the caller before this function's `catch` can see it, so a rejection escapes uncaught here. `return await` waits inside the `try`, turning the rejection into an exception this block can catch.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: That an async function always returns a promise — resolved with the returned value, rejected with an uncaught exception — and that code up to and including the first await runs synchronously.
Check there: That await may be used only inside an async function or at the top level of a module; that control returns to the caller and a microtask is scheduled to continue the paused code; and that a rejected promise makes the await expression throw.
Check there: How the same composition reads with then/catch, for comparison.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown