Tutorials Logic, IN info@tutorialslogic.com

JavaScript Async Await Handle Promises Easily

Async and Await

Async/await is syntax over Promises. An async function always returns a Promise, and `await` pauses only that async function until the awaited value settles. It does not block the JavaScript event loop or remove the need for rejection handling and cancellation.

  • async before a function makes it return a Promise automatically.
  • await pauses the async function until the Promise resolves or rejects.
  • await can only be used inside an async function.
  • Error handling uses standard try/catch blocks.

Basic Async / Await

Basic Async / Await
// async function always returns a Promise
async function greet() {
  return 'Hello!';
}
greet().then(msg => console.log(msg)); // Hello!

// await pauses until the Promise resolves
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user     = await response.json();
  console.log(user.name);
}

fetchUser(1);

Async and Promise Chains

Both approaches do the same thing - async/await is simply easier to read, especially when you have multiple sequential async steps.

Comparison

Comparison
// -- Promise chain --
function loadData() {
  return fetch('/api/user/1')
    .then(r => r.json())
    .then(user => fetch(`/api/posts?userId=${user.id}`))
    .then(r => r.json())
    .then(posts => console.log(posts));
}

// -- Async / Await (same logic, cleaner) --
async function loadData() {
  const userRes  = await fetch('/api/user/1');
  const user     = await userRes.json();

  const postsRes = await fetch(`/api/posts?userId=${user.id}`);
  const posts    = await postsRes.json();

  console.log(posts);
}

Async Error Handling

Use try/catch to handle errors in async functions. Any rejected promise inside the try block will throw and be caught by catch. You can also use finally for cleanup.

try / catch / finally

try / catch / finally
async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);

    if (!res.ok) throw new Error(`HTTP error: ${res.status}`);

    const user = await res.json();
    console.log('User:', user.name);
    return user;

  } catch (error) {
    console.error('Failed to load user:', error.message);

  } finally {
    console.log('Request finished'); // always runs
  }
}

loadUser(42);

Parallel Promise Execution

Using await sequentially means each operation waits for the previous one to finish. When operations are independent, use Promise.all() with await to run them in parallel and save time.

Sequential vs Parallel

Sequential vs Parallel
// Sequential - total time = time1 + time2
async function sequential() {
  const users  = await fetch('/api/users').then(r => r.json());
  const posts  = await fetch('/api/posts').then(r => r.json());
  return { users, posts };
}

// Parallel - total time = max(time1, time2)
async function parallel() {
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
  ]);
  return { users, posts };
}

Async Loops

Be careful when using await inside loops. Using await in a for loop runs iterations sequentially. To run all iterations in parallel, collect the promises first and use Promise.all().

Async in Loops

Async in Loops
const ids = [1, 2, 3, 4, 5];

// Sequential - each waits for the previous
async function loadSequential() {
  for (const id of ids) {
    const user = await fetchUser(id); // waits each time
    console.log(user.name);
  }
}

// Parallel - all fire at once
async function loadParallel() {
  const promises = ids.map(id => fetchUser(id));
  const users    = await Promise.all(promises);
  users.forEach(u => console.log(u.name));
}

// Note: forEach does NOT work with await - use for...of or map
// ids.forEach(async id => { ... }); // won't wait correctly

Async Function Semantics

Calling an `async` function always returns a Promise. Returning a plain value fulfills that Promise, throwing rejects it, and returning another promise adopts that promise's eventual state. The function begins synchronously and runs until the first `await` that cannot continue immediately; its continuation is scheduled as a promise job after the awaited value settles.

`await` applies promise resolution semantics to its operand, so awaiting a plain value still yields asynchronously at the continuation boundary. Other synchronous code in the current job finishes first, then queued microtasks run before the browser selects a later task such as a timer. This ordering explains why replacing `.then()` with `await` can change stack traces and placement without making I/O synchronous.

Only the async function is suspended, not the JavaScript thread. The host can process other jobs while network, storage, or timers progress. CPU-heavy loops still block rendering and input because they do not yield merely because their containing function is async. Split work, use platform scheduling, or move appropriate computation to a worker.

  • Treat every async call as a Promise-returning operation.
  • Remember that code before the first suspension runs synchronously.
  • Use the execution queue to predict logs and state changes.
  • Do not use async as a CPU parallelism mechanism.

Sequential and Concurrent Work

Await operations sequentially when the next input depends on the previous result, when order is a business invariant, or when concurrency would exceed a protected dependency. Independent operations can begin together and then be awaited with `Promise.all`. Starting each promise before awaiting avoids an unnecessary request waterfall.

`Promise.all` fulfills with ordered results when every input fulfills and rejects when one rejects; the other underlying operations are not automatically cancelled. `Promise.allSettled` reports every outcome, `Promise.any` fulfills from the first success and rejects with an aggregate error when all fail, and `Promise.race` settles from the first settled input. Choose from the workflow contract rather than convenience.

Unbounded `map(async ...)` can launch thousands of operations at once. Use a concurrency limiter, worker pool, queue, or chunked loop that matches browser connections, API quotas, memory, and downstream capacity. Preserve item identity with each result so failures can be retried or reported without rerunning successful side effects.

  • Draw dependency edges before choosing sequential or concurrent execution.
  • Bound fan-out from the slowest downstream service.
  • Expect in-flight work to continue after aggregate rejection.
  • Reconcile partial success explicitly.

Cancellation and Deadlines

A Promise has no universal cancel method. Cancellation must be supported by the underlying operation, commonly through `AbortController` and an `AbortSignal`. Pass one signal through every cancellable layer, stop starting new work after abort, and distinguish an intentional abort from a network or application failure in the user interface and telemetry.

A timeout wrapper that merely rejects after a timer does not stop the original fetch, parser, or database call. Combine a timer with abort support, clear the timer in `finally`, and account for the total deadline across retries and nested calls. An inner operation should receive less time than the outer request so cleanup and a useful response remain possible.

React components, route changes, search suggestions, and repeated submissions commonly make older results obsolete. Abort prior work or compare a request generation before applying the result. Otherwise a slow earlier response can overwrite newer state. Cancellation is cooperative, so code between awaited operations must still check the signal where meaningful.

  • Propagate one ownership signal through the full call chain.
  • Cancel underlying work instead of only racing a rejection timer.
  • Remove abort listeners and timers during cleanup.
  • Prevent stale responses from updating current state.

Errors, Retries, and Cleanup

Use `try` and `catch` around the operation whose failure you can handle. A broad catch that converts every error to `null` destroys the distinction between not found, unauthorized, invalid data, cancellation, and infrastructure failure. Add context with a cause when rethrowing, preserve the original stack, and handle an error at one ownership boundary instead of logging it repeatedly at every layer.

Retry only failures classified as transient and only when the operation is safe to repeat. Use bounded attempts, exponential backoff with jitter, a total deadline, and server guidance such as `Retry-After` where applicable. Mutating requests need an idempotency key or reconciliation rule because a timeout may occur after the server committed the change.

`finally` runs after fulfillment or rejection and is appropriate for releasing locks, ending loading state, clearing timers, and closing resources. Avoid returning from `finally` because it can replace the original result or error. When several resources are acquired, clean them up in reverse ownership order and make cleanup tolerate partial initialization.

  • Classify errors before choosing recovery.
  • Keep retries bounded by idempotency and deadline.
  • Preserve the original error as the diagnostic cause.
  • Test cleanup after both success and every failure stage.

Testing and Observability

Test async code by controlling the dependency that settles the Promise, not by sleeping for an arbitrary number of milliseconds. Stub network or storage boundaries with deferred promises so the test can assert loading state, trigger fulfillment or rejection, and then await the observable result. Fake timers help with backoff and deadlines, but promise microtasks may require a separate explicit flush supported by the test framework.

Cover success, validation failure, dependency rejection, timeout, abort, partial aggregate failure, retry exhaustion, and cleanup. Verify both returned results and durable side effects. A test that only expects a rejected Promise can miss a duplicated write that happened before rejection. For concurrent work, force completions in different orders to prove the code does not depend on accidental timing.

Instrument each owned operation with a stable operation ID, start time, attempt number, deadline, dependency, result class, and duration. Do not log the same error as new at every stack layer. The boundary that handles or reports the failure should attach context once, while traces and error causes preserve the chain. Avoid logging secrets or full response bodies simply because parsing failed.

At application boundaries, ensure every Promise has an owner. Event handlers, queue consumers, startup functions, and scheduled tasks should await or explicitly observe the work they launch. Prefixing a call with `void` can document intentional non-awaiting for a linter, but it does not handle rejection or provide lifecycle ownership by itself.

Top-level await is available in modules, but it can delay modules that depend on the current module. Use it for a genuine module initialization dependency, not for unrelated analytics or optional data. When startup can fail, expose a clear failed state, preserve the cause, and avoid leaving half-registered listeners or partially mutated global state.

  • Drive Promise settlement deterministically in tests.
  • Force concurrent completions into more than one order.
  • Give every launched Promise an error and lifecycle owner.
  • Record deadlines and attempts without duplicating error logs.
  • Keep optional startup work outside blocking module evaluation.
  • Verify aborted operations release timers, listeners, and loading state.
  • Measure queue delay separately from dependency execution time.
  • Test retry budgets against the outer deadline.
  • Confirm cancellation is distinct from operational failure.
Before you move on

JavaScript Async Await Handle Promises Easily Mastery Check

5 checks
  • Async functions return Promises, and await resumes after the awaited value settles.
  • It lets you write asynchronous code that looks and reads like synchronous code - no more chaining .then() calls.
  • Under the hood, an async function always returns a Promise, and await pauses execution inside that function until the awaited Promise settles.
  • Async and chained Promise code share settlement semantics, but their control-flow and error boundaries can be organized differently.
  • Use try/catch to handle errors in async functions.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.