Skip to content

Intermediate · Async JavaScript, modules and tools · topic 42 of 64 · 35 min

async/await and error handling

Explanation

async and await let you write promise code that reads like normal top-to-bottom code. An async function always returns a promise, and await pauses only that function until a promise settles. Use try/catch/finally for errors, Promise.all to run independent jobs together, AbortSignal.timeout to give up on slow requests, and a small retry for flaky networks.

async and await

Put async before a function and two things change. First, it always returns a promise, even if you return a plain value. Second, you can use await inside it. await waits for a promise and gives you its value.

javascript
// with then
function loadTrains() {
  return fetch('/api/trains')
    .then((response) => response.json())
    .then((data) => data.trains);
}

// the same with async/await
async function loadTrains() {
  const response = await fetch('/api/trains');
  const data = await response.json();
  return data.trains; // the caller gets a promise of this
}
Same behaviour, but async/await reads top to bottom
Think of it like this: Think of a pressure cooker. Your recipe says "wait for 3 whistles, then open the lid". That recipe pauses at this step (await), but the kitchen does not stop: someone else can still make tea on the other stove. await pauses only your async function, never the whole page.

Errors: try, catch, finally and throw

When an awaited promise rejects, await throws the error, just like a normal error. Catch it with try/catch. finally runs after either. Use throw new Error('clear message') to make your own errors.

javascript
async function showProfile(id) {
  spinner.hidden = false;
  try {
    const response = await fetch(`/api/users/${id}`);
    if (response.status === 404) throw new Error('This user does not exist');
    if (!response.ok) throw new Error(`Server problem (HTTP ${response.status})`);
    const user = await response.json();
    nameEl.textContent = user.name;
  } catch (error) {
    errorEl.textContent = error.message;
  } finally {
    spinner.hidden = true; // always
  }
}
One try block, clear messages, and cleanup in finally
ProblemWhat happensHow you notice
Network error (offline, wrong domain, CORS)fetch rejects with a TypeErrorLands in catch by itself
HTTP error (404, 500 ...)fetch fulfils, response.ok is falseOnly if you check response.ok and throw
Bad JSON in the bodyresponse.json() rejects with a SyntaxErrorLands in catch
Request cancelled or timed outfetch rejects with AbortError or TimeoutErrorCheck error.name in catch

One after another, or all together?

javascript
// Slow: about 1 s + 1 s + 1 s = 3 s
const user = await getJson('/api/user');
const cart = await getJson('/api/cart');
const offers = await getJson('/api/offers');

// Fast: all three start together, about 1 s in total
const [user2, cart2, offers2] = await Promise.all([
  getJson('/api/user'),
  getJson('/api/cart'),
  getJson('/api/offers'),
]);
Await one by one only when a step needs the result of the step before
Careful: Forgetting await is a common bug. const data = getJson(url) gives you a promise, not the data, so data.name is undefined. If a value is unexpectedly a Promise, look for a missing await.

Timeouts: do not wait forever

On a weak mobile network a request can hang for a long time. AbortSignal.timeout(ms) makes a signal that cancels the fetch after ms milliseconds. An AbortController lets you cancel it yourself, for example when the user types a new search.

javascript
try {
  const response = await fetch('/api/pnr/1234567890', {
    signal: AbortSignal.timeout(8000), // give up after 8 seconds
  });
  // ...
} catch (error) {
  if (error.name === 'TimeoutError') showError('The server is slow. Please try again.');
  else showError('Could not load your PNR status.');
}

// Cancel by hand: the old search is not needed any more
const controller = new AbortController();
fetch('/api/search?q=rice', { signal: controller.signal });
controller.abort(); // that fetch rejects with an AbortError
AbortSignal.timeout for a time limit, AbortController to cancel

Retry with a small delay

javascript
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function retry(fn, times, delayMs) {
  for (let attempt = 1; attempt <= times; attempt++) {
    try {
      return await fn(); // success: stop here
    } catch (error) {
      if (attempt === times) throw error; // last try: give up
      await wait(delayMs);
    }
  }
}

const data = await retry(() => getJson('/api/offers'), 3, 500);
Try up to 3 times, waiting half a second between tries
Tip: Retry only safe requests such as GET, and not errors that will never fix themselves (400, 401, 404). Retrying a POST that places an order could place it twice.

Top-level await in modules

In a module script (type="module", next topic) you can use await at the top level, outside any function. In a normal script, await only works inside an async function.

javascript
// main.js, loaded with <script type="module" src="main.js"></script>
const response = await fetch('/config.json');
const config = await response.json();
console.log('Shop name:', config.shopName);
Top-level await works only in modules

Watch the lesson

async/await and error handling · 1/7

async function load() {
  const response = await fetch('/api/trains');
  return response.json();
}

async and await let promise code read like normal code, from top to bottom.

Prefer a video by a teacher? Find free videos on “javascript async await try catch tutorial” on YouTube ↗

In real life: Checking a PNR status on a slow network

A PNR status page calls the server with async/await inside try/catch/finally. It gives up after 8 seconds with AbortSignal.timeout and shows "The server is slow, try again". Because the network on a moving train is patchy, it retries a GET up to 3 times with a short wait. The passenger details and the train running status are loaded together with Promise.all, so the page is ready in one round of waiting, not two.

Practice (0/3 done)

Task 1 · Code · Javascript

Write async function getOrderTotal(fetchOrder, id). fetchOrder(id) returns a promise of an order like { id: "A1", items: [{ name: "Rice", price: 60, qty: 2 }, { name: "Dal", price: 40, qty: 1 }] }. Await it and return the total of price * qty for all items. For that order the answer is 160.

Task 2 · Code · Javascript

Write async function retry(fn, times, delayMs). fn is an async function that sometimes fails (throws). Call it; if it works, return its value at once. If it throws, wait delayMs milliseconds and try again, up to times tries in total. If every try fails, throw the LAST error. Example: a fn that fails twice and then returns "ok" gives "ok" with retry(fn, 3, 10), but throws with retry(fn, 2, 10).

Task 3 · Code · Javascript

Write withTimeout(fn, ms). It calls fn() (which returns a promise) and returns a promise that settles like fn() does, but rejects with new Error("Timeout") if fn() has not settled after ms milliseconds. Use Promise.race between fn() and a timer promise, and clear the timer at the end. Example: a job that takes 150 ms with withTimeout(job, 30) rejects with "Timeout".

Quick quiz

Question 1

1.async function getCount() { return 5; } What does getCount() return?

Question 2

2.Three independent requests each take about 1 second. How long does await Promise.all([a(), b(), c()]) take?

Question 3

3.Which problem reaches your catch block even if you never check response.ok?

Question 4

4.What does fetch(url, { signal: AbortSignal.timeout(5000) }) do?

Question 5

5.const user = getJson('/api/user'); console.log(user.name); prints undefined. Why?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: fetch and APIs