Skip to content
aviral gupta

// I1.3 · ~33 min · Intermediate

async, await and try/catch

After this lesson you write promise code with async and await that reads top to bottom, catch its errors with try/catch/finally, and start independent steps together instead of one by one.

Lesson 3 of 6 in I1 Asynchronous JavaScript

You will be able to

  • Write an async function, read results with await, and say what the function returns
  • Catch a rejection with try/catch around await and clean up with finally
  • Choose between awaiting steps one after another and starting them together
  1. Warm-up · Activity 1 of 7

    Warm-up: what does calling an async function give you?

    async function price() {
      return 5;
    }
    console.log(price());
  2. Predict · Activity 2 of 7

    Predict before you read on. b takes 10 ms, a takes 30 ms. What does this print?

    const later = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
    
    async function main() {
      console.log(await later("a", 30));
      console.log(await later("b", 10));
    }
    main();
    console.log("main called");
  3. Practice · Activity 3 of 7

    Fill in the keyword that lets this function use await.

    ____ function load() {
      const value = await Promise.resolve("data");
      return value;
    }
    function load() {
  4. Practice · Activity 4 of 7

    The awaited promise rejects. What does this print?

    async function save() {
      try {
        await Promise.reject(new Error("disk full"));
        console.log("saved");
      } catch (error) {
        console.log("error:", error.message);
      } finally {
        console.log("cleanup");
      }
    }
    save();
  5. Practice · Activity 5 of 7

    Inside an async function, match each expression to what it gives.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. isAdmin() returns false for Ada. What does this print?

    async function isAdmin(user) {
      return user === "root";
    }
    
    if (isAdmin("ada")) {
      console.log("welcome, admin");
    } else {
      console.log("welcome, user");
    }
  7. Apply · Activity 7 of 7

    Mini-task. Write async function loadPage(failSidebar) that prints spinner on, loads an article (30 ms) and a sidebar (10 ms) at the same time with a promise helper wait(ms, value, fail), prints show article and sidebar, and prints spinner off whatever happens. If the sidebar fails, print error: and its message instead of the show line. Run it once with each argument.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

One by one, together, and a caught failure

step() logs when it starts and ends and settles after a delay. oneByOne() awaits a, then b: b starts only after a has ended. together() starts c and d at once and awaits Promise.all, so the faster d ends first, yet the results keep their order. safely() awaits a step that fails inside try: catch and finally run. main.js is a module, so it awaits at the top level.

main.js

// A step that logs when it starts and ends, and settles after ms milliseconds.
const step = (name, ms, fail = false) =>
  new Promise((resolve, reject) => {
    console.log(`start ${name}`);
    setTimeout(() => {
      console.log(`end ${name}`);
      if (fail) reject(new Error(`${name} failed`));
      else resolve(name.toUpperCase());
    }, ms);
  });

async function oneByOne() {
  const a = await step("a", 30); // b does not start until a has ended
  const b = await step("b", 10);
  return [a, b];
}

async function together() {
  const [c, d] = await Promise.all([step("c", 30), step("d", 10)]); // both start now
  return [c, d];
}

async function safely() {
  try {
    await step("e", 5, true);
    console.log("not reached");
  } catch (error) {
    console.log("caught:", error.message);
  } finally {
    console.log("finally runs");
  }
}

console.log(await oneByOne()); // top-level await: main.js is a module
console.log(await together());
await safely();

Run it with

node main.js

Output

start a
end a
start b
end b
[ 'A', 'B' ]
start c
start d
end d
end c
[ 'C', 'D' ]
start e
end e
caught: e failed
finally runs
  • One by one: start b comes after end a, so the waits add up.
  • Together: both start before either ends; the slower one sets the time.
  • Promise.all keeps the input order: [ 'C', 'D' ], although d ended first.
  • “not reached” is skipped: the await threw into catch, then finally ran.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Await inside the try

profileLine(api) returns a promise of a line such as Ada (3 posts). api.user() gives {id, name}; api.posts(id) gives an array of posts. If api.posts rejects, the line is Ada (posts unavailable); if api.user rejects, profileLine rejects with that error. The starter forgets one await and so prints undefined posts and never catches. Fix it with await and try/catch.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: the first one says undefined posts. What is posts without await?

  2. Hint 2

    A promise has no length, and a rejection that is never awaited inside the try cannot reach the catch.

  3. Hint 3

    Write const posts = await api.posts(user.id); inside the try.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export async function profileLine(api) {
  const user = await api.user();
  try {
    const posts = await api.posts(user.id);
    return `${user.name} (${posts.length} posts)`;
  } catch {
    return `${user.name} (posts unavailable)`;
  }
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export async function profileLine(api) {
  const user = await api.user();
  try {
    const posts = api.posts(user.id);
    return `${user.name} (${posts.length} posts)`;
  } catch {
    return `${user.name} (posts unavailable)`;
  }
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {profileLine} from './main.js';

const later = (value, fail = false) =>
  new Promise((resolve, reject) => setTimeout(() => (fail ? reject(value) : resolve(value)), 5));

test('Ada with three posts gives Ada (3 posts)', async () => {
  const api = {user: () => later({id: 1, name: 'Ada'}), posts: () => later(['a', 'b', 'c'])};
  const got = await profileLine(api);
  assert.equal(got, 'Ada (3 posts)', `profileLine gave ${got}`);
});

test('Failing posts give Ada (posts unavailable)', async () => {
  const api = {user: () => later({id: 1, name: 'Ada'}), posts: () => later(new Error('timeout'), true)};
  const got = await profileLine(api);
  assert.equal(got, 'Ada (posts unavailable)', `profileLine gave ${got}`);
});

test('A failing user rejects with the same error', async () => {
  const api = {user: () => later(new Error('no user'), true), posts: () => later([])};
  await assert.rejects(profileLine(api), {message: 'no user'}, 'profileLine should reject with the user error');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Start them all at once

loadAll(loaders) gets an array of functions; each returns a promise. It must return a promise of all their results, in the same order. The starter awaits each loader in a loop, so each one starts only after the one before has finished. The results are right, but slow. Start every loader first, then wait for all of them together.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: the log shows start 0, end 0, start 1. Where does the loop wait?

  2. Hint 2

    Call every loader first: loaders.map((load) => load()) gives an array of promises that are already running.

  3. Hint 3

    Then wait for all of them at once with await Promise.all(…), which keeps their order.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export async function loadAll(loaders) {
  return Promise.all(loaders.map((load) => load()));
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export async function loadAll(loaders) {
  const results = [];
  for (const load of loaders) {
    results.push(await load());
  }
  return results;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadAll} from './main.js';

// Each loader logs when it starts and ends; later ones are faster.
const makeLoaders = (log) =>
  [30, 20, 10].map((ms, i) => () => {
    log.push(`start ${i}`);
    return new Promise((resolve) => setTimeout(() => {
      log.push(`end ${i}`);
      resolve(`r${i}`);
    }, ms));
  });

test('The results come back in input order', async () => {
  const got = await loadAll(makeLoaders([]));
  assert.deepEqual(got, ['r0', 'r1', 'r2'], `loadAll gave ${JSON.stringify(got)}`);
});

test('Every loader starts before any of them ends', async () => {
  const log = [];
  await loadAll(makeLoaders(log));
  assert.deepEqual(log.slice(0, 3), ['start 0', 'start 1', 'start 2'], `the order was ${log.join(', ')}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

await in a function that is not async

function loadName() {
  const name = await Promise.resolve("Ada");
  return name;
}
console.log(loadName());

What Node.js prints

SyntaxError: Unexpected reserved word

Why, and the fix

In a module, await is a reserved word, so a plain function that uses it does not even compile, and nothing runs. Mark the function async: async function loadName() { … }. Then remember that its caller gets a promise, so it needs await loadName() too.

A forgotten await

async function getPrice() {
  return 4.5;
}
const total = getPrice() + 1;
console.log(total);
console.log(total.toFixed(2));

What Node.js prints

TypeError: total.toFixed is not a function

Why, and the fix

getPrice() returns a promise, and + turns that promise into the text [object Promise], so total is the string "[object Promise]1", and strings have no toFixed. The first line prints it without any error. Write const total = (await getPrice()) + 1;.

Two started promises, awaited one by one

const wait = (ms, fail) =>
  new Promise((resolve, reject) => setTimeout(() => (fail ? reject(new Error("fast one failed")) : resolve("ok")), ms));

async function both() {
  const slow = wait(50, false);
  const fast = wait(10, true);
  try {
    return [await slow, await fast];
  } catch (error) {
    return "handled: " + error.message;
  }
}
console.log(await both());

What Node.js prints

Error: fast one failed

Why, and the fix

fast rejects after 10 ms, while the function is still waiting for slow, so nothing handles fast yet. Node.js treats that as an unhandled rejection and stops the program; the catch never runs and “handled” is never printed. MDN warns about exactly this. Await both at once: return await Promise.all([slow, fast]); inside the try.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

async returns a promise, await reads it

An async function always returns a promise: return 5 fulfils it with 5, and a throw rejects it. Inside, await p pauses the function until p settles, then gives back the value, or throws the reason. Code after an await runs later, as a then() callback would (lesson I1.1). await works only inside an async function or at the top level of a module; main.js is a module, so it may await directly. Awaiting a plain value such as 42 just gives 42, a moment later.

try, catch, finally around await

Because await throws the rejection reason, ordinary try/catch handles async errors: the rest of the try block is skipped and catch gets the error. finally runs whether or not anything was thrown, which makes it the place to switch a spinner off or close a file. A catch only sees what is awaited inside its try. Forget the await and the value is a promise: getPrice() + 1 is the string "[object Promise]1", and if (isAdmin(user)) is always true, because a promise object is truthy.

One after another, or together

const a = await stepA(); const b = await stepB(); starts stepB only after stepA has finished, so the times add up. That is right when B needs A’s result. When the steps are independent, start them first and wait for all of them: await Promise.all([stepA(), stepB()]) takes as long as the slowest. Do not start two promises and then await them one by one: if the second rejects while the first is still pending, MDN warns that the rejection is unhandled, whatever catch the caller has.

Sources

Last reviewed October 5, 2026