JS Story

JS Story: Async Programming

callbacks, promises, async/await, fetch, patterns

5.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.

5.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 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, errors repeated at each level
    });
  });
});

5.2 Promises

A Promise is an object representing the eventual result of an async operation. It is always in one of three states: pending, fulfilled (succeeded with a value), or rejected (failed with a reason).

5.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`));

5.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

5.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()),
]);

5.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.
async function loadDashboard(userId) {
  const user   = await getUser(userId);
  const orders = await getOrders(user.id);
  const items  = await getItems(orders[0].id);
  render(items);
}
// reads top-to-bottom; equivalent to the .then chain above

5.4 Error Handling

Use try/catch inside async functions:
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:
window.addEventListener('unhandledrejection', e => {
  console.error('Unhandled rejection:', e.reason);
});

5.5 The Fetch API

fetch(url, options) is the standard way to make HTTP requests from the browser. It returns a Promise for a Response object. Key detail: fetch only rejects on network failure - always check resp.ok for HTTP error status codes like 404 or 500.
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
}

// POST with JSON body
const resp = await fetch('/api/items', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body:    JSON.stringify({ name: 'widget' }),
});
Common fetch options and Response members
Member Purpose
fetch() options
methodHTTP verb: GET (default), POST, PUT, PATCH, DELETE.
headersObject or Headers instance of request headers.
bodyRequest body: string, FormData, Blob, ReadableStream. Not allowed on GET/HEAD.
credentialsomit | same-origin (default) | include - whether to send cookies cross-origin.
modecors (default) | no-cors | same-origin - CORS policy.
cachedefault | no-store | reload | force-cache.
signalAbortSignal from an AbortController to cancel the request.
Response members
resp.oktrue when status is 200-299.
resp.statusHTTP status code (200, 404, 500, ...).
resp.json()Parse body as JSON; returns a Promise.
resp.text()Read body as a UTF-8 string; returns a Promise.
resp.blob()Read body as a Blob; returns a Promise.
resp.arrayBuffer()Read body as an ArrayBuffer; returns a Promise.

5.5.1 Cancelling a Request

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

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

5.6 Common Async Patterns

Sequential steps - each step uses the result of the previous:
const a = await step1();
const b = await step2(a);
const c = await step3(b);
Parallel requests - all start at once, wait for all to finish:
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
Retry on failure - attempt up to n times before re-throwing:
async function withRetry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try { return await fn(); }
    catch (e) { if (i === attempts - 1) throw e; }
  }
}
Debounce - defer async work until the user stops typing:
let timer;
input.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(() => search(input.value), 300);
});
Sequential array processing - one item at a time to avoid overload:
for (const id of ids) {
  await processOne(id);   // waits for each before starting the next
}
Parallel array processing - all items at once:
await Promise.all(ids.map(id => processOne(id)));

5.7 Tutorials

Async JavaScript Tutorials
Tutorial Type Why Use It
MDN: Asynchronous JavaScript Beginner→Intermediate Complete series from callbacks through async/await with live 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.

5.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.