Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk5 min

JavaScript Promises Inside Promises: Why They Break and How to Fix Them

A promise returned from .then() becomes part of the chain. Learn why missing returns detach work, how catch changes outcomes, and how to choose async patterns.
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(), the usual cause is that the inner asynchronous operation was started but not returned. Returning it connects the work to the chain: the next promise adopts its eventual fulfillment or rejection. For ordinary sequences, use a flat promise chain or async/await; reserve promise combinators for independent tasks.

What does “a promise inside a promise” mean?

The phrase can describe two related behaviors. A promise may be resolved with another promise, or a .then() handler may return a promise. In both cases, JavaScript follows the inner promise’s eventual state rather than treating that promise as an ordinary fulfillment value.

As an Amazon Associate I earn from qualifying purchases.

Promise resolution adopts the inner promise

When an outer promise is resolved with an inner promise, the outer promise becomes locked to follow it. It can be considered resolved while still pending: it does not fulfill until the inner promise fulfills, and it rejects if the inner one rejects. This behavior is called promise adoption or assimilation. See MDN’s Promise reference.

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

A returned promise becomes part of the chain

Every call to .then() creates a new promise. If its handler returns a promise or thenable, that new chain promise adopts the returned value’s eventual state. A rejection therefore continues down the chain unless a handler handles it.

By contrast, starting asynchronous work without returning it detaches that work from the chain. The next handler can run before it finishes, and the outer chain does not automatically wait for or catch its rejection.

// Detached: the chain does not wait for saveRecord or catch its rejection.
getRecord(id).then((record) => {
  saveRecord(record); // Missing return
}).then(() => showSaved());

// Connected: the next step waits for saveRecord; failures reach catch.
getRecord(id)
  .then((record) => saveRecord(record))
  .then(() => showSaved())
  .catch(reportFailure);

Return the asynchronous operation from the handler, or return an async function call. That makes the chain’s result represent the whole sequence.

Do not wrap an existing promise without a reason

return new Promise((resolve) => resolve(otherPromise)) does not preserve a useful extra layer: the outer promise adopts the inner one. Wrapping a function that already returns a promise in a new Promise is usually the Promise constructor antipattern. Use the constructor when you genuinely need to bridge a callback-based API or another boundary that does not already provide a promise.

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

Why do promise callbacks run “out of order”?

The executor passed to new Promise() runs during construction. Handlers registered with .then(), however, do not run inline. Once the relevant promise settles, its reactions are queued as jobs. A handler added to an already-settled promise is queued too, not called synchronously. Chained handlers then run in dependency order.

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 queued promise reactions run. An await also defers the async function’s continuation, even when the awaited value is already fulfilled. It suspends only that function’s dependent code; the rest of the program continues. MDN notes that “the await expression never blocks the main thread”.

Promises coordinate asynchronous processes; they are not lazy computations and do not create parallel CPU execution. I/O operations may overlap, but ordinary JavaScript main-thread code still runs one task at a time.

Common nested-promise bugs and their fixes

1. Starting work but forgetting to return it

Symptom: A later handler runs before a request or save finishes, or its rejection bypasses the chain’s .catch().

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

Fix: Return the operation: return fetch(url), return save(record), or return loadData(). A concise arrow handler such as .then(record => saveRecord(record)) returns its expression automatically.

2. Using a promise as if it were its value

Symptom: A log shows a pending Promise, or code reads a property before the operation fulfills.

Fix: In an async function, use const value = await getValue(); in a chain, use .then(value => ...). await gives you the fulfillment value and throws at that expression if the awaited promise rejects.

3. Catching an error and unintentionally recovering

A .catch() handler is a recovery handler. If it logs an error and returns normally, the promise produced by that handler fulfills with the returned value—often undefined—so later chain steps continue.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
loadData()
  .catch((error) => {
    console.error(error);
    throw error; // Preserve the rejection for a later handler.
  })
  .then(renderData)
  .catch(showFailure);

Return an explicit fallback only when continuing is intended. Otherwise rethrow the error or return a rejected promise. A local catch can also be useful for optional work: it can supply a fallback for that work while leaving failures in the critical path to an outer catch. MDN’s guide to using promises recommends keeping ordinary promise chains flat and using nesting only when it deliberately limits a recovery boundary.

4. Expecting try/catch to catch an un-awaited rejection

A synchronous try/catch around a call to an async function does not catch a rejection that happens later if the returned promise is not awaited:

async function load() {
  try {
    const data = await getData();
    return data;
  } catch (error) {
    reportFailure(error);
    throw error;
  }
}

Alternatively, attach .catch() to the returned promise. Also, if a function can throw synchronously while it is being called, that throw happens before a chained .catch() can be attached; put the invocation inside a try/catch when that possibility matters.

5. Serializing independent operations by accident

Awaiting each independent request inside a loop starts the next only after the previous one finishes. Start the operations independently and select the combinator that matches the outcome you need:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Need Pattern Outcome
Every independent operation must fulfill Promise.all() Fulfills with the values when all fulfill; rejects if an input rejects.
Inspect every success and failure Promise.allSettled() Waits for all outcomes instead of short-circuiting on rejection.
Use the first successful result Promise.any() Fulfills on the first fulfillment; rejects if all inputs reject.
Use whichever operation settles first Promise.race() Adopts the first settlement, whether fulfillment or rejection.

These methods do not cancel inputs that lose the race or are no longer needed. If the underlying API supports cancellation, use its cancellation mechanism, such as an AbortSignal.

6. Assuming a timeout race stops the slower operation

Promise.race() settles the promise returned by the race; it does not stop the other input. If a timeout wins, a network request or other underlying operation may keep running. Pair the operation with a cancellation mechanism supported by that API when stopping it matters. Promises do not have a general cancellation protocol.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Should you use a chain, async/await, or a combinator?

Situation Clear choice Why
Step B needs the result of step A Flat .then() chain or sequential await Makes the dependency explicit and lets the returned chain represent the sequence.
Several operations are independent and all are required Promise.all() Collects their values on success and rejects if an input rejects.
Every independent outcome must be reported Promise.allSettled() Waits for all outcomes.
Any one successful result is enough Promise.any() Settles successfully on the first fulfillment.
The first settlement should decide Promise.race() Uses the first fulfillment or rejection, without canceling other inputs.
Optional work may fail without aborting the main task Local .catch() or inner try/catch Defines a deliberate, narrow recovery boundary.

For deeper reference, MDN’s JavaScript promises guide covers chaining and nesting, while its await reference explains what is suspended and how rejection is handled.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.