Tutorials Logic, IN info@tutorialslogic.com

JavaScript Promises then, catch, async/await

What is a Promise?

A Promise is a JavaScript object that represents the eventual result of an asynchronous operation. Instead of passing callbacks into a function, a promise lets you attach handlers to the future success or failure of that operation. This makes async code much easier to read and reason about.

A Promise is always in one of three states:

Once a promise is fulfilled or rejected it is settled and its state never changes again.

  • Pending - the initial state; the operation has not completed yet.
  • Fulfilled - the operation completed successfully and the promise has a resolved value.
  • Rejected - the operation failed and the promise has a reason (error).

Creating a Promise

Creating a Promise
const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve('Operation succeeded!');
  } else {
    reject(new Error('Operation failed!'));
  }
});

promise
  .then(result => console.log(result))   // Operation succeeded!
  .catch(error => console.error(error));  // only runs on rejection

Chaining Promises

Each .then() returns a new promise, which allows you to chain multiple async steps in a readable sequence. The value returned from one .then() is passed as the argument to the next.

Promise Chaining

Promise Chaining
fetch('https://api.example.com/user/1')
  .then(response => response.json())       // parse JSON
  .then(user => {
    console.log(user.name);
    return fetch(`/api/posts?userId=${user.id}`);
  })
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error('Error:', error))
  .finally(() => console.log('Done'));     // always runs

Promise.all() - Run in Parallel

Promise.all() takes an array of promises and returns a single promise that resolves when all of them resolve, or rejects as soon as any one of them rejects. Use it when tasks are independent and can run simultaneously.

Promise.all()

Promise.all()
const p1 = fetch('/api/users').then(r => r.json());
const p2 = fetch('/api/posts').then(r => r.json());
const p3 = fetch('/api/comments').then(r => r.json());

Promise.all([p1, p2, p3])
  .then(([users, posts, comments]) => {
    console.log(users, posts, comments);
  })
  .catch(err => console.error('One failed:', err));

Promise.allSettled(), race(), any()

JavaScript provides several other static methods for handling multiple promises:

  • Promise.allSettled() - waits for all promises to settle (fulfilled or rejected); never rejects. Returns an array of result objects.
  • Promise.race() - resolves or rejects as soon as the first promise settles.
  • Promise.any() - resolves as soon as the first promise fulfills; rejects only if all reject.

allSettled / race / any

allSettled / race / any
const slow = new Promise(res => setTimeout(() => res('slow'), 2000));
const fast = new Promise(res => setTimeout(() => res('fast'), 500));
const fail = Promise.reject(new Error('failed'));

// allSettled - never rejects
Promise.allSettled([slow, fast, fail]).then(results => {
  results.forEach(r => console.log(r.status, r.value ?? r.reason));
});

// race - first to settle wins
Promise.race([slow, fast]).then(v => console.log(v)); // 'fast'

// any - first to FULFILL wins
Promise.any([fail, fast]).then(v => console.log(v));  // 'fast'

Error Handling in Promises

Always attach a .catch() at the end of a promise chain to handle any rejection that bubbles up. The .finally() handler runs regardless of success or failure - useful for cleanup like hiding a loading spinner.

Error Handling

Error Handling
function loadUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    });
}

loadUser(42)
  .then(user => console.log('Loaded:', user.name))
  .catch(err => console.error('Failed:', err.message))
  .finally(() => console.log('Request complete'));

Promise State and Resolution

A Promise is pending until it becomes fulfilled or rejected. Settled means fulfilled or rejected. Resolved is broader: a promise can be resolved to another pending promise and adopt its eventual state. Calls to resolve or reject after the first resolution attempt have no effect on the outcome, although executor code can still run unless it returns.

The Promise constructor executor runs synchronously. Use it to adapt a callback-style operation that does not already return a Promise, and catch synchronous exceptions through the constructor’s rejection behavior. Do not wrap an existing Promise merely to call its `then` and `catch`; return or chain the original operation.

Promise resolution assimilates thenables by reading and invoking a callable `then` according to the language algorithm. This interoperability is useful but means a hostile or broken thenable can throw while its property is read. Prefer real Promises from trusted APIs and treat arbitrary thenables as an integration boundary.

A promise represents one eventual outcome and can be observed by several consumers. It is not a lazy task by default; the executor has already begun. Use a function that creates and returns a Promise when each caller should start fresh work.

  • Distinguish pending, settled, fulfilled, rejected, and resolved.
  • Remember that constructor executors run immediately.
  • Return existing Promises instead of wrapping them unnecessarily.
  • Use a function when work should start on demand.

Chaining and Value Flow

Every call to `then`, `catch`, or `finally` returns a new Promise. A fulfillment handler’s returned value fulfills the next promise; a thrown error rejects it; a returned Promise delays and determines it. Forgetting `return` inside a handler breaks the dependency chain and lets the next step run with `undefined` before nested work finishes.

A rejection travels through fulfillment-only handlers until a rejection handler processes or rethrows it. A catch that returns a value converts the chain back to fulfillment. Use that only when the value is a valid recovery result; otherwise throw the original or a contextual error with `cause`.

`finally` observes settlement without receiving the fulfillment value or rejection reason as a normal argument. If it completes, the prior outcome passes through. If it throws or returns a rejecting Promise, the new failure replaces the prior outcome. Use it for cleanup, not for transforming successful data.

Handlers run as queued promise jobs after the current synchronous job, even when the Promise was already settled. This consistent asynchronous delivery prevents one API from sometimes calling back immediately and sometimes later, but it also means state read immediately after registering a handler has not yet been updated by that handler.

  • Return every nested asynchronous dependency from its handler.
  • Recover in catch only with a valid substitute outcome.
  • Use finally for cleanup without replacing the result.
  • Predict handler order from the microtask queue.

Promise Composition

`Promise.all` preserves input order in its result and rejects after the first observed rejection, but it does not cancel the remaining work. Use it when every result is required. Attach cancellation through the underlying APIs when failure of one input should stop others, and still observe cleanup for operations already in flight.

`Promise.allSettled` waits for every input and returns status objects, which is useful for independent batch items. `Promise.any` returns the first fulfillment and rejects with `AggregateError` only when all inputs reject. `Promise.race` follows the first settlement, including a rejection. None of these methods creates concurrency; the input operations generally start when their Promises are created.

An empty iterable has API-specific behavior: `all` and `allSettled` fulfill with empty arrays, while `any` rejects with an aggregate error and `race` remains pending. Handle dynamically generated empty sets deliberately rather than leaving a workflow waiting on a race that can never settle.

Limit concurrency for large collections. Composition over thousands of already-started fetches can exhaust connections and memory. A worker pool should start only the allowed number, preserve result identity, stop or continue according to policy, and expose partial outcomes without replaying completed side effects.

  • Choose the combinator from required success semantics.
  • Handle empty inputs explicitly.
  • Do not mistake fail-fast reporting for cancellation.
  • Bound operation creation as well as result collection.

Ownership, Testing, and Diagnostics

Every Promise needs an owner that returns it, awaits it, or attaches a rejection handler. Fire-and-forget work still needs lifecycle, cancellation, and telemetry ownership. A global unhandled-rejection listener can report mistakes but cannot restore business consistency or decide whether an abandoned operation should continue.

Test Promise code by controlling settlement. Use deferred test dependencies to assert the state before completion, then fulfill or reject and await the public result. Force different completion orders for composed work. Avoid arbitrary sleeps that make the suite slow and still fail under load.

Preserve operation IDs, attempt numbers, deadlines, and cause chains in diagnostics. Do not log at every `catch` and then rethrow unchanged. Report once at the boundary that decides the outcome, while lower layers add context through typed errors or `cause`.

A Promise has no universal cancellation operation. Pass `AbortSignal` to APIs that support it and make custom producers stop resources and reject or settle according to their contract. Racing a timeout Promise changes which outcome the caller sees but does not stop the losing operation.

  • Return or observe every Promise created by application code.
  • Settle test dependencies deterministically.
  • Report a failure once at its owning boundary.
  • Cancel the underlying operation, not only the waiting Promise.

Promise API Design

Return a Promise from an asynchronous API rather than accepting both a callback and returning a second completion channel. Document fulfillment value, rejection categories, cancellation input, timeout ownership, and whether calling the function starts new work. Avoid functions that sometimes return a plain value and sometimes a Promise because callers must then reason about two timing contracts.

Cache a fulfilled data value according to freshness policy, not a rejected Promise forever. An in-flight Promise can deduplicate identical concurrent reads, but remove or replace it after failure according to retry policy. Include tenant, authorization, locale, and request options in the cache key so one caller never receives another caller’s result.

Expose a factory function for repeatable operations. Reusing one Promise reuses one outcome; it does not rerun the executor. For polling or retry, call the factory under a bounded policy. Keep retries outside the low-level Promise constructor so attempts, backoff, cancellation, and final failure remain observable.

When adapting an event emitter or subscription, a single Promise can represent only one completion. Return an async iterator, stream, or unsubscribe-capable subscription for multiple values. Converting the first event to a Promise without removing listeners leaks resources and loses later errors.

  • Publish one consistent asynchronous completion channel.
  • Deduplicate only requests with the same full security context.
  • Use factories when operations must run again.
  • Use streams or iterators for multiple values.
Before you move on

JavaScript Promises then, catch, async/await Mastery Check

5 checks
  • A Promise is a JavaScript object that represents the eventual result of an asynchronous operation.
  • Instead of passing callbacks into a function, a promise lets you attach handlers to the future success or failure of that operation.
  • This makes async code much easier to read and reason about.
  • Log a pending promise through fulfillment and rejection paths without assuming synchronous completion.
  • A settled Promise keeps its outcome, while each handler creates a separate downstream Promise.
Browse Free Tutorials

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