Async and await in JavaScript, with live examples

A Promise is a value that arrives later. async and await let you write code that waits for it as if it were ordinary step-by-step code, while the page keeps running.

A Promise stands for a value that is not ready yet, such as a server's answer. An async function always returns a promise. Inside it, await pauses that one function until the promise settles and hands you the value. The page keeps running.

Try it first. The three tasks below are fake requests made with setTimeout. Run them one by one, then together, then as a race, and compare the total time.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>await vs Promise.all vs Promise.race</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .btns { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
  button { font: inherit; padding: 8px 12px; border: 1px solid #c9ced8; border-radius: 8px; background: #fff; cursor: pointer; }
  button:disabled { opacity: .5; cursor: default; }
  .row { display: grid; grid-template-columns: 96px 1fr; align-items: center; gap: 8px; margin: 8px 0; font-size: 14px; }
  .track { position: relative; height: 26px; background: #fff; border: 1px solid #e1e4ea; border-radius: 6px; overflow: hidden; }
  .bar { position: absolute; top: 3px; bottom: 3px; width: 0; border-radius: 4px; background: #93b4f5; }
  .bar.done { background: #2f9e5b; }
  .bar.ignored { background: #c7ccd4; }
  pre { margin: 12px 0 8px; padding: 10px; background: #1d2330; color: #e5e9f0; border-radius: 8px; font-size: 13px; overflow-x: auto; min-height: 4.5em; line-height: 1.5; }
  #total { font-weight: 700; min-height: 1.3em; }
</style>
</head>
<body>
<div class="btns">
  <button data-mode="seq">One by one (await)</button>
  <button data-mode="all">Promise.all</button>
  <button data-mode="race">Promise.race</button>
</div>
<div id="rows"></div>
<pre id="code">Pick a button. Each task is a fake request made with setTimeout.</pre>
<div id="total"></div>

<script>
  // A promise that resolves after ms milliseconds
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  const tasks = [
    { name: 'Load user', ms: 900 },
    { name: 'Load posts', ms: 600 },
    { name: 'Load photos', ms: 1200 },
  ];
  const SCALE = 2800; // the track width stands for 2.8 seconds
  const code = {
    seq: 'for (const t of tasks) {\n  await fakeTask(t); // next one waits\n}',
    all: '// all three start now\nawait Promise.all(\n  tasks.map(fakeTask));',
    race: '// first to settle wins\nconst first = await Promise.race(\n  tasks.map(fakeTask));',
  };

  const rows = document.getElementById('rows');
  const bars = tasks.map((t) => {
    rows.insertAdjacentHTML('beforeend', `<div class="row"><span>${t.name}</span><div class="track"><div class="bar"></div></div></div>`);
    return rows.lastElementChild.querySelector('.bar');
  });

  let t0 = 0;
  // Fake request: draws its own bar while it runs
  async function fakeTask(t) {
    const bar = bars[tasks.indexOf(t)];
    const start = performance.now() - t0;
    bar.style.left = (start / SCALE * 100) + '%';
    const grow = setInterval(() => {
      bar.style.width = ((performance.now() - t0 - start) / SCALE * 100) + '%';
    }, 16);
    await wait(t.ms);
    clearInterval(grow);
    bar.style.width = (t.ms / SCALE * 100) + '%';
    bar.classList.add('done');
    return t.name;
  }

  async function run(mode) {
    bars.forEach((b) => { b.className = 'bar'; b.style.width = '0'; });
    document.getElementById('code').textContent = code[mode];
    const total = document.getElementById('total');
    total.textContent = 'Running...';
    t0 = performance.now();
    let note = '';
    let pending = [];

    if (mode === 'seq') {
      for (const t of tasks) await fakeTask(t);
    } else if (mode === 'all') {
      await Promise.all(tasks.map(fakeTask));
    } else {
      pending = tasks.map(fakeTask);
      note = ' - winner: ' + await Promise.race(pending);
      // The losers keep running; race just stops listening to them
      bars.forEach((b) => { if (!b.classList.contains('done')) b.classList.add('ignored'); });
    }
    total.textContent = 'Total: ' + Math.round(performance.now() - t0) + ' ms' + note;
    await Promise.all(pending); // let the losers finish before the next run
  }

  document.querySelectorAll('button').forEach((btn) => {
    btn.addEventListener('click', async () => {
      document.querySelectorAll('button').forEach((b) => (b.disabled = true));
      await run(btn.dataset.mode);
      document.querySelectorAll('button').forEach((b) => (b.disabled = false));
    });
  });
</script>
</body>
</html>
Three fake requests of 0.9s, 0.6s and 1.2s. One by one adds up, Promise.all takes the slowest, Promise.race takes the fastest.

What a Promise is

A promise is in one of three states. It starts pending. It then becomes fulfilled with a value or rejected with an error, and it never changes after that. Settled is the word for either of the last two.

You rarely build promises by hand, because fetch and many browser APIs already return them. The one you will write most is a delay helper, which turns setTimeout into something you can await:

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

async function start() {
  console.log('one');
  await wait(1000);          // this function pauses for a second
  console.log('two');
}
start();
console.log('this runs before "two"');

The last line runs straight away. Only start is paused, not the page. Every demo on this page uses the same trick to fake slow work, so none of them need a network.

async functions always return a promise

Put async in front of a function and its return value is wrapped in a promise, even if you return a plain string. A throw inside it becomes a rejected promise. That is why the most common async bug looks like this on the page:

Without await you print the promise itself. With await you print the value.
Without await you print the promise itself. With await you print the value.

[object Promise] means the code used the promise where it wanted the value. Add await in front of the call. If the calling code is not async, mark it async too, or use .then(value => ...).

await is only allowed inside an async function or at the top level of a module. Anywhere else the browser stops with a SyntaxError before any of the script runs.

Top-level await in modules

In a module you can write await outside any function. The module waits at that line before running the rest.

<script type="module">
  const res = await fetch('data.json');
  const data = await res.json();
  document.querySelector('h1').textContent = data.title;
</script>

Module scripts are deferred, so the HTML above them has been parsed by the time they run. In a normal <script>, the same code is a SyntaxError.

Outside a module, wrap it in async function main() { ... } main(); instead. For turning response text into an object, see JSON.parse.

Catching errors with try/catch

When an awaited promise rejects, the await line throws. Normal try/catch catches it, which is the main reason async/await reads more easily than chains of .then() and .catch().

async function load() {
  try {
    const data = await getData();
    show(data);
  } catch (err) {
    showError(err.message);   // the request failed
  }
}

A rejected promise that nobody awaits or catches is an unhandled rejection. The browser logs it as an error in the console, and your page silently skips whatever was meant to happen next.

Try the three buttons below. Set the failure rate to Sometimes and press each one a few times.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Async errors: try/catch, allSettled, forgotten await</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; font-size: 14px; }
  .btns { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 12px; }
  button, select { font: inherit; padding: 8px 12px; border: 1px solid #c9ced8; border-radius: 8px; background: #fff; }
  button { cursor: pointer; }
  table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; }
  th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid #e8eaef; }
  th { background: #eef1f5; font-size: 13px; }
  .ok { color: #0f7a3d; font-weight: 600; }
  .bad { color: #b4380f; font-weight: 600; }
  #msg { margin-top: 10px; padding: 10px; border-radius: 8px; background: #fff; border: 1px solid #e1e4ea; min-height: 1.4em; }
  code { font-family: ui-monospace, Consolas, monospace; font-size: 13px; }
</style>
</head>
<body>
<label>Each fake request fails:
  <select id="rate">
    <option value="0.4">Sometimes (40%)</option>
    <option value="0">Never</option>
    <option value="1">Always</option>
  </select>
</label>
<div class="btns">
  <button id="all">try/catch + Promise.all</button>
  <button id="settled">Promise.allSettled</button>
  <button id="forgot">Forgot await</button>
</div>
<table>
  <thead><tr><th>Request</th><th>Result</th></tr></thead>
  <tbody id="rows"><tr><td colspan="2">Press a button.</td></tr></tbody>
</table>
<div id="msg"></div>

<script>
  const names = ['user', 'posts', 'photos'];
  const rows = document.getElementById('rows');
  const msg = document.getElementById('msg');

  // Fake request: random delay, fails at the chosen rate
  function fakeRequest(name) {
    const rate = Number(document.getElementById('rate').value);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (Math.random() < rate) reject(new Error(name + ': server error'));
        else resolve(name + ' loaded');
      }, 200 + Math.random() * 500);
    });
  }

  function show(list) {
    rows.innerHTML = list.map(([name, text, ok]) =>
      `<tr><td>${name}</td><td class="${ok ? 'ok' : 'bad'}">${text}</td></tr>`).join('');
  }

  // 1. Promise.all inside try/catch: one failure throws, all results are lost
  document.getElementById('all').addEventListener('click', async () => {
    msg.textContent = 'Loading...';
    try {
      const results = await Promise.all(names.map(fakeRequest));
      show(results.map((r, i) => [names[i], r, true]));
      msg.textContent = 'All three succeeded.';
    } catch (err) {
      show(names.map((n) => [n, 'no result (Promise.all rejected)', false]));
      msg.textContent = 'Caught: ' + err.message;
    }
  });

  // 2. allSettled never rejects: one row per request, success or failure
  document.getElementById('settled').addEventListener('click', async () => {
    msg.textContent = 'Loading...';
    const results = await Promise.allSettled(names.map(fakeRequest));
    show(results.map((r, i) => r.status === 'fulfilled'
      ? [names[i], r.value, true]
      : [names[i], r.reason.message, false]));
    const failed = results.filter((r) => r.status === 'rejected').length;
    msg.textContent = failed + ' of ' + results.length + ' failed. The others still count.';
  });

  // 3. The bug: without await you get the Promise object, not the value
  document.getElementById('forgot').addEventListener('click', async () => {
    const wrong = fakeRequest('user');
    wrong.catch(() => {}); // this demo ignores its failure on purpose
    const text = 'Hello ' + wrong;
    msg.innerHTML = 'Without await: <code></code>';
    msg.querySelector('code').textContent = text;
    try {
      const right = await fakeRequest('user');
      show([['user (no await)', text, false], ['user (await)', right, true]]);
    } catch (err) {
      show([['user (no await)', text, false], ['user (await)', err.message, false]]);
    }
  });
</script>
</body>
</html>
Fake requests that fail at the rate you choose. Promise.all loses everything on one failure, allSettled keeps every result, and the last button shows the missing-await bug.

In sequence or in parallel

Calling an async function starts the work immediately. await only decides when you wait for it. So three awaits in a row run one after another, while starting all three first and then waiting lets them overlap.

Three awaits in a row add up. Promise.all waits only as long as the slowest request.
Three awaits in a row add up. Promise.all waits only as long as the slowest request.

Use one after another when a step needs the previous result, for example loading a user and then that user's posts. Otherwise, start them together with one of the four combinators:

Method Settles when Result Good for
Promise.all All fulfil, or the first one rejects Array of values Everything must succeed
Promise.allSettled Every promise has settled Array of { status, value } or { status, reason } Show what worked, report what failed
Promise.race The first one settles, success or failure That one value or error Timeouts
Promise.any The first one fulfils That value, or an AggregateError if all reject Several sources, first good answer wins

None of them cancel the promises they stop listening to. In the race demo above, the slower bars keep running to the end. Their results are simply ignored.

await in a loop

for...of with await inside runs the loop body one item at a time. That is correct when order matters, but with fifty items it is fifty waits in a row.

for...of waits for each, map with Promise.all runs them together, and forEach does not wait at all.
for...of waits for each, map with Promise.all runs them together, and forEach does not wait at all.
// One at a time: total = sum of all
for (const id of ids) {
  await save(id);
}

// Together: total = the slowest one
await Promise.all(ids.map((id) => save(id)));

Avoid forEach with an async callback. It ignores the promises the callback returns, so code after the loop runs before any item is done.

Timeouts and cancelling

Promise.race with the delay helper gives any promise a time limit:

const timeout = (ms) => new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Timed out')), ms));

const data = await Promise.race([getData(), timeout(5000)]);

That stops the waiting, not the work. To actually stop a request, use an AbortController. You pass its signal to the request and call abort() when you no longer need the answer. fetch then rejects with an error whose name is 'AbortError'.

const controller = new AbortController();
const res = await fetch('/api/search?q=pear', { signal: controller.signal });
// elsewhere, when the user types something newer:
controller.abort();

For a plain time limit on fetch, AbortSignal.timeout(5000) makes a signal that aborts by itself. In that case the error's name is 'TimeoutError'.

A search box puts all of this together. Typing sends a request, the answer arrives later, and an older, slower answer must not overwrite a newer one.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Async search box with cancel</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; font-size: 14px; }
  input[type=search] { width: 100%; box-sizing: border-box; font: inherit; font-size: 16px; padding: 10px 12px; border: 1px solid #c9ced8; border-radius: 8px; }
  .opt { display: block; margin: 8px 0; color: #4b5563; }
  #status { min-height: 1.4em; margin: 8px 0; }
  #status.loading::before { content: ''; display: inline-block; width: 10px; height: 10px; margin-right: 6px; border: 2px solid #93b4f5; border-top-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }
  #status.error { color: #b4380f; font-weight: 600; }
  @keyframes spin { to { transform: rotate(360deg); } }
  ul { list-style: none; margin: 0; padding: 0; background: #fff; border-radius: 8px; border: 1px solid #e1e4ea; min-height: 40px; max-height: 190px; overflow: auto; }
  li { padding: 7px 12px; border-bottom: 1px solid #eef0f3; }
  #log { margin-top: 10px; font: 12px/1.5 ui-monospace, Consolas, monospace; color: #6b7280; max-height: 92px; overflow: auto; }
</style>
</head>
<body>
<input type="search" id="q" placeholder="Search fruit (type 'oops' for an error)" autocomplete="off">
<label class="opt"><input type="checkbox" id="cancel" checked> Cancel stale requests (AbortController)</label>
<div id="status">Start typing.</div>
<ul id="results"></ul>
<div id="log"></div>

<script>
  const FRUIT = ['Apple', 'Apricot', 'Avocado', 'Banana', 'Blackberry', 'Blueberry', 'Cherry', 'Coconut',
    'Date', 'Fig', 'Grape', 'Grapefruit', 'Guava', 'Kiwi', 'Lemon', 'Lime', 'Lychee', 'Mango', 'Melon',
    'Nectarine', 'Orange', 'Papaya', 'Peach', 'Pear', 'Pineapple', 'Plum', 'Pomegranate', 'Raspberry', 'Strawberry'];

  // Fake search API. Short queries are slower, like a real server with more matches.
  // It listens to an AbortSignal and gives up when told to.
  function fakeSearch(query, signal) {
    return new Promise((resolve, reject) => {
      const ms = Math.max(200, 1800 - query.length * 600) + Math.random() * 150;
      const timer = setTimeout(() => {
        if (query.includes('oops')) return reject(new Error('Search is down. Try again.'));
        resolve(FRUIT.filter((f) => f.toLowerCase().includes(query.toLowerCase())));
      }, ms);
      signal?.addEventListener('abort', () => {
        clearTimeout(timer);
        reject(new DOMException('Cancelled', 'AbortError'));
      });
    });
  }

  const input = document.getElementById('q');
  const status = document.getElementById('status');
  const list = document.getElementById('results');
  const log = (text) => document.getElementById('log').insertAdjacentHTML('afterbegin', text + '<br>');
  let controller = null;
  let debounce = 0;
  let id = 0;

  async function search(query) {
    const n = ++id;
    if (document.getElementById('cancel').checked) {
      controller?.abort();               // cancel the previous request
      controller = new AbortController();
    } else {
      controller = null;
    }
    status.className = 'loading';
    status.textContent = 'Searching for "' + query + '"...';
    const started = performance.now();
    try {
      const found = await fakeSearch(query, controller?.signal);
      list.replaceChildren(...found.map((f) => {
        const li = document.createElement('li');
        li.textContent = f;
        return li;
      }));
      status.className = '';
      status.textContent = found.length + ' results for "' + query + '" in ' + Math.round(performance.now() - started) + ' ms';
      log('#' + n + ' "' + query + '" shown');
    } catch (err) {
      if (err.name === 'AbortError') return log('#' + n + ' "' + query + '" cancelled');
      status.className = 'error';
      status.textContent = err.message;
      list.innerHTML = '';
      log('#' + n + ' "' + query + '" failed');
    }
  }

  // Debounce: wait until typing pauses for 300 ms
  input.addEventListener('input', () => {
    clearTimeout(debounce);
    const query = input.value.trim();
    if (!query) { controller?.abort(); list.innerHTML = ''; status.className = ''; status.textContent = 'Start typing.'; return; }
    debounce = setTimeout(() => search(query), 300);
  });
</script>
</body>
</html>
Debounced fake search with a loading state, an error state (type "oops") and cancelled stale requests. Untick the box, type "a", pause, then type "p" to see an old answer overwrite a new one.
  • Debounce: each input event, attached with addEventListener, resets a 300 ms timer. The search runs only when typing pauses.

  • Loading state: set a "Searching..." message before the await, replace it after.

  • Cancel stale requests: before each new search, call abort() on the previous controller. The cancelled search lands in catch with AbortError and quietly returns.

  • Errors: anything else that lands in catch is shown as a message instead of an empty list.

If you cannot pass a signal to the work you are waiting for, a request counter does the same job. Keep a number, increase it for each search, and ignore any answer whose number is not the latest.

When it does not work

What you see Cause Fix
[object Promise] on the page The promise was used without await await the call, or use .then()
"Uncaught (in promise)" in the console A rejected promise with no catch try/catch around the await, or .catch()
A SyntaxError that mentions await await in a normal function or classic script Mark the function async, or use type="module"
A loop of requests takes ages await inside for...of runs them one by one map plus Promise.all when they are independent
Code after the loop runs too early forEach with an async callback for...of or Promise.all
Old search results replace new ones A slower, older request finished last AbortController or a request counter
One failure loses every result Promise.all rejects on the first failure Promise.allSettled

For scripts that do nothing at all, HTML JavaScript not working goes through the other usual causes.

Async behaviour is hard to explain with a screenshot. Loading states, errors and the order results arrive in only show up when the page runs.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can press the buttons and type into the search box themselves. If you change the code later, the same link shows the new version.

Questions people ask

Does an async function always return a promise?

Yes. Whatever you return is wrapped in a promise, and anything thrown inside becomes a rejected promise. To get the value, await the call inside another async function or a module, or use .then().

Does await block the whole page?

No. await pauses only the async function it is in. Clicks, timers, animations and other functions keep running. The paused function continues when the promise settles.

Can I use await outside an async function?

In a classic script, no. It is a SyntaxError. At the top level of a module, such as <script type="module">, yes. Everywhere else, wrap the code in an async function.

What is the difference between Promise.all and Promise.allSettled?

Promise.all rejects as soon as one promise rejects, so you lose the other results. Promise.allSettled always waits for every promise and gives you an object per promise with status "fulfilled" and a value, or "rejected" and a reason.

Why does my loop with await take so long?

An await inside for...of waits for each item before starting the next, so the total time is the sum of all of them. If the items do not depend on each other, start them with map and await Promise.all instead.

Keep reading