WebDev Story

WebDev Story: JavaScript Async

callbacks, promises, async/await, fetch

8.0 The Problem: Blocking Code

JavaScript runs on a single thread. A long synchronous operation — reading a file, querying a database, waiting for a network response — blocks every other operation until it finishes. The page freezes; the user sees no response to clicks or key presses.
// bad: synchronous network (hypothetical — blocks the thread)
const data = fetchSync('/api/items');    // nothing else can run here
render(data);
JavaScript solves this with asynchronous patterns that let the engine continue processing other events while waiting for a slow operation to complete. Three patterns exist, in historical order: callbacks, Promises, and async/await.

8.1 Callbacks

The original async pattern: pass a function to be called when the operation completes.
setTimeout(() => {
  console.log('ran after 1 second');
}, 1000);

// Node-style: error-first callback
fs.readFile('data.txt', 'utf8', (err, text) => {
  if (err) { console.error(err); return; }
  console.log(text);
});
Callbacks work, but nesting them for sequential async steps produces deeply indented, hard-to-follow code known as callback hell:
getUser(id, (err, user) => {
  if (err) return handleErr(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleErr(err);
    getItems(orders[0].id, (err, items) => {
      if (err) return handleErr(err);
      render(items);   // three levels deep, and errors repeat
    });
  });
});

8.2 Promises

A Promise is an object representing the eventual result of an async operation. It is always in one of three states:
  • pending — the operation has not completed yet.
  • fulfilled — the operation succeeded; the Promise holds its result value.
  • rejected — the operation failed; the Promise holds a reason (usually an Error).

8.2.1 Creating a Promise

function delay(ms) {
  return new Promise((resolve, reject) => {
    if (ms < 0) { reject(new Error('negative delay')); return; }
    setTimeout(() => resolve(ms), ms);
  });
}

delay(500).then(ms => console.log(`done after ${ms}ms`));
The constructor receives an executor function with two callbacks: call resolve(value) on success, reject(reason) on failure.

8.2.2 Chaining

.then(onFulfilled) returns a new Promise, enabling flat sequential chains that replace nested callbacks:
getUser(id)
  .then(user   => getOrders(user.id))
  .then(orders => getItems(orders[0].id))
  .then(items  => render(items))
  .catch(err   => handleErr(err));    // one error handler for the whole chain
.catch(fn) is shorthand for .then(undefined, fn). A .finally(fn) block runs regardless of outcome and is useful for cleanup.

8.2.3 Promise Combinators

Promise.all, race, allSettled, any
Combinator Behavior
Promise.all(iterable) Fulfills when all fulfill; rejects as soon as any one rejects. Result is an array of values in input order.
Promise.allSettled(iterable) Always fulfills when every Promise settles. Result is an array of {status, value|reason} objects — no early rejection.
Promise.race(iterable) Settles with the first Promise to settle, fulfilled or rejected.
Promise.any(iterable) Fulfills with the first fulfilled value. Rejects with AggregateError only if all reject.
Promise.resolve(value) Returns a Promise already fulfilled with value.
Promise.reject(reason) Returns a Promise already rejected with reason.
// fetch three resources in parallel, wait for all
const [users, posts, tags] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/tags').then(r => r.json()),
]);

8.3 async / await

async/await is syntactic sugar over Promises that lets you write async code with the same linear structure as synchronous code.
  • Marking a function async makes it always return a Promise.
  • await expr pauses the function (not the thread) until the Promise resolves, then evaluates to the fulfilled value.
  • await may only appear inside an async function or at the top level of a module.
async function loadDashboard(userId) {
  const user   = await getUser(userId);
  const orders = await getOrders(user.id);
  const items  = await getItems(orders[0].id);
  render(items);
}
This is exactly equivalent to the .then chain above, but reads top-to-bottom like synchronous code.

8.4 Error Handling

Use try/catch inside async functions instead of .catch() on a chain:
async function loadDashboard(userId) {
  try {
    const user   = await getUser(userId);
    const orders = await getOrders(user.id);
    render(orders);
  } catch (err) {
    showError(err.message);
  } finally {
    hideSpinner();
  }
}
Always handle rejections. An unhandled Promise rejection triggers the browser's unhandledrejection event and in Node logs a warning or crashes the process:
window.addEventListener('unhandledrejection', e => {
  console.error('Unhandled rejection:', e.reason);
});

8.5 The Fetch API

fetch(url, options) is the modern way to make HTTP requests from the browser. It returns a Promise for a Response object.
async function getItems() {
  const resp = await fetch('/api/items');
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
  return resp.json();          // .json() also returns a Promise
}
Key detail: fetch only rejects on network failure, not on HTTP error status codes like 404 or 500. Always check resp.ok (or resp.status) explicitly.

8.5.1 Fetch Options

const resp = await fetch('/api/items', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body:    JSON.stringify({ name: 'widget' }),
  signal:  controller.signal,    // AbortController for cancellation
});
Common fetch options
Option Purpose
methodHTTP verb: GET (default), POST, PUT, PATCH, DELETE.
headersObject or Headers instance of request headers.
bodyRequest body: string, FormData, Blob, ReadableStream, etc. Not allowed on GET/HEAD.
credentialsomit | same-origin (default) | include — whether to send cookies cross-origin.
modecors (default) | no-cors | same-origin — CORS policy for the request.
cachedefault | no-store | reload | force-cache | only-if-cached.
signalAbortSignal from an AbortController to cancel the request.

8.5.2 Reading the Response

Response methods and properties
Member Purpose
resp.oktrue when status is 200–299.
resp.statusHTTP status code (200, 404, 500, …).
resp.headersHeaders object; use .get('content-type') to read.
resp.json()Parse body as JSON; returns a Promise.
resp.text()Read body as a UTF-8 string; returns a Promise.
resp.formData()Parse body as FormData; returns a Promise.
resp.blob()Read body as a Blob; returns a Promise.
resp.arrayBuffer()Read body as an ArrayBuffer; returns a Promise.
resp.bodyReadableStream for streaming large responses.

8.5.3 Cancelling a Request

const controller = new AbortController();

// cancel after 5 seconds
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const resp = await fetch('/api/slow', { signal: controller.signal });
  const data = await resp.json();
  render(data);
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('request was cancelled');
  } else {
    throw err;
  }
} finally {
  clearTimeout(timeout);
}

8.6 Common Async Patterns

Async pattern recipes
Pattern Code
Sequential steps
const a = await step1();
const b = await step2(a);
const c = await step3(b);
Parallel requests
const [a, b] = await Promise.all([
  fetchA(), fetchB()
]);
Retry on failure
async function withRetry(fn, n = 3) {
  for (let i = 0; i < n; i++) {
    try { return await fn(); }
    catch (e) { if (i === n-1) throw e; }
  }
}
Debounce async
let timer;
input.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(() => search(input.value), 300);
});
Sequential array
for (const id of ids) {
  await processOne(id);   // one at a time
}
Parallel array
await Promise.all(
  ids.map(id => processOne(id))  // all at once
);

8.7 Tutorials

Async JavaScript Tutorials
Tutorial Type Why Use It
MDN: Asynchronous JavaScript Beginner→Intermediate Complete series from callbacks through async/await with examples.
javascript.info: Async/await Intermediate Clear, concise explanations with runnable examples for each concept.
MDN: Using the Fetch API Intermediate Comprehensive guide to fetch options, response handling, and error patterns.
javascript.info: Promises Beginner→Intermediate Four-part series covering creation, chaining, error handling, and combinators.
web.dev: JavaScript Promises Intermediate Thorough treatment of promise states, chaining, and parallelism.
Jake Archibald: Event Loop (talk) Deep dive Visual explanation of how the event loop, tasks, and microtasks interact.

8.8 References

Async JavaScript References
Resource Description
MDN: Promise Full API reference for Promise, including all static methods and instance methods.
MDN: async function Reference for async function syntax, return values, and interaction with await.
MDN: Fetch API Complete reference for fetch(), Request, Response, and Headers.
MDN: AbortController API for cancelling fetch requests and other async operations.
ECMAScript spec: Promises Formal specification of the Promise abstract operations and state machine.
WHATWG: Event Loops Authoritative specification of the event loop, task queues, and microtask checkpoint.