Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If a promise inside a .then() seems to run out of order or escape your .catch(), check whether you returned it. Every .then() creates a new promise, and that promise waits for a promise or thenable returned by its handler. Start asynchronous work without returning it, however, and the outer chain cannot wait for that work or handle its rejection.

What “a promise inside a promise” actually means

The phrase can describe two related behaviors. A promise can be resolved with another promise, or a .then() handler can return a promise. In both cases, JavaScript adopts the inner promise’s eventual state rather than fulfilling the outer promise with the inner Promise object as an ordinary value. The outer promise may be resolved—locked to follow the inner one—while still pending; if the inner promise rejects, the outer promise follows that rejection. See MDN’s Promise reference.

That behavior is why a returned promise usually does not produce a visible promise-within-a-promise. The promise created by .then() adopts the returned promise or thenable. The important distinction is whether the handler returns the asynchronous operation at all.

Missing return detaches work from the chain

Here, saveRecord() starts, but its promise is not returned. The next handler can run without waiting for the save, and a rejection from the save is not automatically represented by the outer chain:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
getRecord(id).then((record) => {
  saveRecord(record); // missing return
}).then(() => showSaved());

Return the operation to connect its completion and failure to the chain:

getRecord(id)
  .then((record) => saveRecord(record))
  .then(() => showSaved())
  .catch(reportFailure);

MDN’s Using promises guide recommends keeping ordinary promise chains flat: “Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition.” Wrapping an already promise-returning function in new Promise() usually adds no useful layer, because resolution adopts the returned promise. The Promise constructor is better reserved for bridging a callback-based API or another genuine API boundary.

Why promise code appears to run out of order

A Promise constructor’s executor runs as part of creating the promise. By contrast, callbacks registered with .then() run later as queued jobs, not inline—even if the promise is already settled. Dependent handlers then proceed through the chain:

Promise.resolve()
  .then(() => console.log("first"))
  .then(() => console.log("second"));
console.log("sync");
// sync, first, second

The synchronous log appears first because the current JavaScript work finishes before the promise reactions run. The first handler runs before the second because the second depends on the promise returned by the first .then(). Independent sibling handlers follow their registration order; they do not wait for one another unless you connect them through a returned promise.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An await similarly suspends only the continuation of its surrounding async function until the awaited value settles. Other program work continues. Even awaiting an already fulfilled value defers that function’s continuation. await accepts promises, thenables, and ordinary values; if the awaited value rejects, the rejection is thrown at the await expression. As MDN puts it in its await reference, “The await expression never blocks the main thread and only defers execution of code that actually depends on the result, i.e., anything after the await expression.”

Promises coordinate asynchronous processes that are already underway; they do not make CPU work run in parallel. I/O operations may overlap, but JavaScript still executes ordinary main-thread tasks one at a time.

Common promise bugs and their fixes

Starting work without returning it

  • Symptom: A later handler runs before a request or save finishes, or the outer .catch() misses that operation’s rejection.
  • Fix: Return the promise from the handler: return fetch(...), return save(...), or return loadData(). An expression-bodied arrow such as record => saveRecord(record) returns its expression automatically.

Using a promise as though it were its value

  • Symptom: A log shows a pending Promise, or code reads a property before the asynchronous result is available.
  • Fix: Consume the fulfillment value inside .then(value => ...) or assign it with const value = await getValue() inside an async function. Awaiting also throws when the promise rejects.

Recovering accidentally in .catch()

A catch handler is a recovery point. If it handles an error, logs it, and returns normally, the promise produced by that handler fulfills with the returned value (or undefined). Later chain steps therefore run as fulfilled steps. Return a fallback only when recovery is intended; otherwise rethrow the error so the chain remains rejected:

operation()
  .catch((error) => {
    logError(error);
    throw error; // preserve rejection for a later handler or caller
  });

Local recovery can be useful when a failure is optional rather than critical. For example, a failure to load a nonessential preference can be caught near that operation and replaced with a default, while failures in the main task continue to an outer catch. The recovery boundary should match which failure the program can safely tolerate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Expecting try/catch to catch an un-awaited rejection

A try block does not catch a later rejection merely because an async function was called inside it. Await the promise inside the block, or attach a .catch() to it:

try {
  const result = await loadData();
  use(result);
} catch (error) {
  reportFailure(error);
}

There is a related edge case: if calling a function throws synchronously before a promise is returned, a catch attached afterward with .catch() cannot catch that throw. Put the invocation inside try/catch when it may throw synchronously.

Serializing independent work by awaiting in a loop

If each request is independent, awaiting each one before starting the next needlessly makes the code sequential. Start the operations and choose a combinator that matches the outcome you need:

Situation Pattern Outcome
Step B needs the result of step A Flat .then() chain or sequential await Preserves the dependency and makes the resulting chain or function continuation represent the sequence.
Independent operations; every one must fulfill Promise.all() Fulfills with all values if all inputs fulfill; rejects when an input rejects.
Independent operations; inspect every outcome Promise.allSettled() Waits for every input to settle and reports each result.
Use the first successful result Promise.any() Fulfills on the first fulfillment; rejects if all inputs reject.
Use whichever input settles first Promise.race() Adopts the first settled input’s state; does not cancel the others.
An optional operation may fail without stopping critical work Local .catch() or inner try/catch Limits recovery to the optional operation.

These combinators coordinate outcomes; they do not cancel losing or remaining operations. If an operation should stop after a timeout or another result wins, use a cancellation mechanism supported by that API, such as an AbortSignal where available. Promises have no general cancellation protocol.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose a flat chain or async/await for sequential tasks

When one step needs another step’s result, use a flat .then() chain or sequential await. In either style, make the dependency explicit and let errors propagate to a recovery point that can handle them. For independent work, use the combinator that matches whether you need all successes, every outcome, the first success, or the first settlement.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.