Agentic AI with Java: Live Cohort
JavaScriptAsynchronous JavaScript

async/await and Cancellation

Write clear asynchronous flows, handle failures, and cancel stale work.

An async function always returns a promise. await pauses that function until a promise settles; it does not block the entire JavaScript runtime. Rejection becomes a thrown error at the await expression.

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

async function greetLater(name) {
  await delay(100);
  return `Hello, ${name}`;
}

try {
  console.log(await greetLater("Asha"));
} catch (error) {
  console.error(error);
}

Run this as an ES module (.mjs or type: module), because it uses top-level await. If two independent tasks can start together, create both promises before awaiting them:

const first = delay(100).then(() => "one");
const second = delay(100).then(() => "two");
console.log(await Promise.all([first, second]));

AbortController supplies a signal used by APIs such as fetch. Aborting is cooperative: it requests cancellation from code that understands the signal. It does not magically stop every promise or undo server-side work.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
  const response = await fetch("https://example.com/", { signal: controller.signal });
  console.log(response.status);
} finally {
  clearTimeout(timeout);
}

In a UI, cancel an old request when a new search starts or the view closes. Always handle the rejected promise after aborting; distinguish an abort from a real network failure when showing a message.

Error handling and cleanup

await throws when the awaited promise rejects, so ordinary try/catch works naturally. Catch an error only when the current function can recover or translate it; otherwise let it propagate to a boundary that can show a message, retry, or report failure. finally is useful for cleanup such as hiding a loading indicator, but make sure it runs for the same operation that started that indicator.

async function loadProfile(id) {
  try {
    const response = await fetch(`/api/profiles/${encodeURIComponent(id)}`);
    if (!response.ok) throw new Error(`Request failed: ${response.status}`);
    return await response.json();
  } catch (error) {
    throw new Error("Could not load profile", { cause: error });
  }
}

When wrapping an error, preserve the original as cause so diagnostics retain the underlying failure. Do not return a fake success value from catch unless callers can distinguish that fallback from real data.

Sequential versus concurrent work

The following two shapes look similar but behave differently when tasks are independent:

const a = await delay(100);
const b = await delay(100); // starts only after the first delay finishes

const firstTask = delay(100);
const secondTask = delay(100); // starts before awaiting firstTask
await Promise.all([firstTask, secondTask]);

The first waits roughly twice as long; the second overlaps the waits. Use sequential await when the second step depends on the first. Use Promise.all() when both must succeed and can start together. Starting a promise creates work immediately; if you start several, make sure every rejection is eventually handled.

try/finally is useful for cleanup regardless of success or failure. In the fetch example, the timer is cleared even if the request rejects. An AbortSignal can be shared with multiple supported operations, but aborting it cannot be undone; create a new controller for a new request.

If a user changes a search query quickly, a response from the old request may arrive after the new one. Cancel the old request or compare a request ID before displaying results. Otherwise stale data can overwrite the latest state despite every individual request succeeding.

Practice: Change the timeout to 1 millisecond and catch the error. Check its name before deciding whether to display it to a user.

Check: An aborted fetch normally rejects with an error whose name is "AbortError". A timeout of 1 ms is not a deterministic test of remote speed; use a controlled test server when you need to test cancellation reliably.

Cancellation design

Cancellation is a request, not forced termination. Check signal.aborted in long-running loops and pass the signal down to APIs that support it. When writing your own async function, decide how it responds to an already-aborted signal and whether it cleans up listeners when work completes. An AbortSignal is one-shot; a canceled signal cannot be reset.

When several operations share one controller, aborting it requests cancellation of all of them. Promise.all() does not do this automatically. If a group operation should stop its siblings after one fails, explicitly abort the shared controller and still await or handle every started task.

Written By: Shiva Srivastava

How is this guide?

Last updated on