cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

How promises work inside

Learn how promise reaction lists, microtask jobs, thenable adoption, extra ticks, and rejection tracking shape JavaScript order.

By the end, you can
  • 01
    Trace a pending promiseExplain how then saves reaction records and why settling schedules jobs rather than calling handlers inline.
  • 02
    Recognize adoption workPredict when a promise or thenable needs a PromiseResolveThenableJob and therefore more microtask turns.
  • 03
    Handle rejections deliberatelyExplain what browsers and Node report when a rejected promise has no handler at the end of a job.

A promise has waiting lists

A promise looks small in app code. You create one, attach then or catch, and wait. Inside the language model, that small object remembers its state, its result, and the reactions that are waiting for that result.

Definition

A promise reaction is the saved work created by then or catch. It becomes a later job; it does not run in the stack that added it.

One ready order, one later reactionPop out in the code editor (opens in a new tab)JavaScript
const order = Promise.resolve("burger");order.then((food) => console.log(food));console.log("waiting");

Line 1 creates an order that is already fulfilled with "burger". Line 2 adds a reaction that will log the food. Line 3 logs synchronously, so the real output is waiting and then burger.

The everyday lessons Promises, Promise chaining, Microtasks, and Build a promise teach how to use these APIs. This lesson keeps that order-token model and looks one layer lower.

States and reaction records

Pending means the order has not settled. Fulfilled means it has a value. Rejected means it has a reason. A promise begins pending, settles once, and never changes to a different state afterward.

Two people join one pending orderJavaScript
const order = makeOrder(); order.then((food) => console.log("Asha gets", food));order.then((food) => console.log("Noah gets", food)); order.fulfill("burger");runJobs();

Line 1 creates the pending teaching-model order. Lines 3 and 4 add Asha and Noah to its reaction list. Line 6 fulfills the order, moving both saved reactions into jobs. Line 7 drains those jobs, so the visible order is Asha first and Noah second.

Real-life analogyMcDonald's order token

Inside the kitchen, imagine that an order token has a list of people waiting for it. When the food is ready, staff call each person in the order they joined.

In real life: A token is waiting on the screen
In JavaScript: A promise is pending
In real life: People wait for the same order
In JavaScript: then adds reaction records
In real life: Staff calls people in join order
In JavaScript: Jobs preserve reaction order
In real life: The food is ready once
In JavaScript: A promise settles once

Where the analogy stops: A real restaurant does not implement JavaScript jobs; the analogy only explains saved waiting work and order.

A reaction record is more than a function name in the real specification. It also connects to the promise created by that then call. The teaching model keeps only the part we need here: a handler, an order, and a jobs queue.

PromiseReactionJob

A PromiseReactionJob is the queued work that runs one ready reaction. When a pending promise settles, every waiting reaction becomes one of these jobs. When you call then on an already settled promise, its reaction can become a job straight away.

A settled order still waits for its handlerPop out in the code editor (opens in a new tab)JavaScript
const order = Promise.resolve("burger"); order.then((food) => console.log("first", food));console.log("after then");

Line 1 creates a fulfilled order. Line 3 adds its reaction, which is queued instead of called inline. Line 4 therefore prints first. The real output is after then, then first burger.

Step through two waiting reactions
Step 0 of 7Ready
Your turn: follow the blue line

Replay the reaction-list teaching model: handlers become jobs only when the pending order settles.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 order.then((food) => console.log("Asha gets", food));order.then((food) => console.log("Noah gets", food)); order.fulfill("burger");runJobs();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay instruments the small model from the source file. First it stores Asha, then Noah. Settlement changes the model state and fills its jobs queue. Draining that queue produces Asha before Noah, just as the order-token list promised.

This separation protects ordinary JavaScript reasoning. The code that calls fulfill can finish its own work before a reaction starts. A handler can add more promise work, return another promise, or throw; each outcome gets its own continuation rules instead of interrupting the statement that settled the first order.

Registration order matters only among reactions that are ready in the same queue. It is not a promise that every unrelated browser action waits behind them. The next lesson on the microtask queue will place these jobs beside rendering, timers, and host checkpoints.

PromiseResolveThenableJob

A thenable is an object or function with a callable then method. It is promise-like, but it is not necessarily a native promise. That matters because its then method is ordinary user-provided code.

A tiny thenable adoption modelJavaScript
const order = makeOrder();const kitchenToken = {  then(resolve) {    resolve("burger");  },}; resolveOrder(order, kitchenToken);runJobs();

Line 1 makes the outer pending order. Lines 3 through 5 make a plain object with then. Line 8 resolves the outer order with that object, so the model queues a PromiseResolveThenableJob. Line 9 drains it; only then does the outer order receive burger.

Step through thenable adoption
Step 0 of 6Ready
Your turn: follow the blue line

Replay a thenable-adoption model. The job boundary explains why promise-like values can add observable turns.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const kitchenToken = {  then(resolve) {    resolve("burger");  },}; resolveOrder(order, kitchenToken);runJobs();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The separate job is useful because thenables can throw, resolve twice, or come from code you do not control. The resolution procedure has rules for those cases. Our model stays small, but it preserves the important boundary: adopting a thenable is extra queued work.

Native promises are designed to cooperate with this procedure too. That is why a library can return a promise-like object without every caller needing special syntax. The trade-off is that adopting another result is not the same small path as storing a plain string.

When you debug an ordering problem, do not start by blaming the thenable. First ask whether the value being resolved is plain, native-promise-shaped, or a custom object with then. That one question often explains why a familiar callback moved to a later turn.

Why promise resolution takes extra ticks

Resolving with a plain value and resolving with another promise look similar at a call site. They are not identical internally. The second case needs the outer promise to follow the inner promise, so jobs can be added before the outer reaction is ready.

Compare value and promise resolutionPop out in the code editor (opens in a new tab)JavaScript
const later = Promise.resolve("tea");const valueOrder = new Promise((resolve) => resolve("value"));const promiseOrder = new Promise((resolve) => resolve(later)); valueOrder.then(() => console.log("value arrives"));promiseOrder.then(() => console.log("promise arrives")); for (let tick = 1; tick <= 2; tick += 1) {  Promise.resolve().then(() => console.log("tick", tick));}

Line 1 makes the inner ready promise. Lines 2 and 3 call fresh promise resolvers, once with a value and once with that inner promise. Lines 5 and 6 schedule their output reactions. Lines 8 through 10 add tick markers. In the tested output, value arrives appears before tick 1, while promise arrives appears two ticks later.

Do not turn this into a stopwatch rule. The markers are promise jobs that make one ordering relationship easy to see. The lesson test proves this exact snippet's printed order, not a timing promise for every engine or application.

The important comparison is relative. Both results are asynchronous. The plain-value order can become fulfilled during its resolver call, so its reaction is ready immediately. The promise-value order must first follow later; that adoption creates more work before its reaction can be queued.

This is also why copying a short output sequence from a console can mislead. Add your own small marker jobs, like this lesson does, and compare the whole sequence. A marker is easier to reason about than milliseconds, which can vary with a busy device.

Promise work that is easy to confuse
ThingWhen it existsWhat it does
Reaction recordSaved while a promise is pendingThe handler and the promise created for its result
PromiseReactionJobQueued when a reaction can runCalls the saved handler in a microtask
PromiseResolveThenableJobQueued when resolving with a thenableCalls the thenable's then safely before adoption
Host rejection reportAfter a rejected promise stays unhandledA browser event or Node process event

Unhandled rejection tracking

Unhandled rejection tracking is host behavior around a rejected promise that still has no rejection handler at the end of a job checkpoint. The language defines promise behavior; browsers and Node decide how to notify developers about a missing owner.

Browser rejection listenersPop out in the code editor (opens in a new tab)JavaScript
window.addEventListener("unhandledrejection", (event) => {  console.log("unhandled", event.reason);  event.preventDefault();}); const order = Promise.reject("sold out");queueMicrotask(() => order.catch(() => console.log("handled later")));

Lines 1 through 4 register a browser listener and prevent its default report. Line 6 creates a rejected order. Line 7 attaches a handler in a later microtask. This browser-runnable example demonstrates registration and a later handler without leaving a deliberately unhandled rejection in the editor.

Node child-process rejection evidenceJavaScript
process.on("unhandledRejection", (reason) => {  console.log("unhandled", reason);}); Promise.reject("sold out");setTimeout(() => console.log("done"), 0);

Line 1 registers Node's unhandledRejection listener. Line 5 rejects with sold out. The lesson test runs this in a child process and proves stable output: unhandled sold out, then done. Attach handlers in real app code instead of depending on host reports.

A browser can dispatch rejectionhandled when a promise that was reported later gains a handler. That event is useful diagnostic evidence, not a pattern for delaying error handling. A request should normally have a clear failure path before users can interact with its result.

Node policies and browser console presentation can differ, which is why this lesson proves only the stable listener output from its own child process. The rule your application controls is simpler: return the promise to an owner or attach the handler yourself.

Compare resolution inputs

Use one input at a time. A plain value has no adoption work. A native promise and a thenable both ask the outer promise to follow another result. The playground runs real promises and prints marker jobs beside the final arrival.

Playground: choose what resolve receives
The selected resolution inputJavaScript
const received = selected === "value"  ? "burger"  : selected === "promise"    ? Promise.resolve("burger")    : { then(resolve) { resolve("burger"); } }; new Promise((resolve) => resolve(received))  .then((food) => console.log("food", food));Promise.resolve().then(() => console.log("tick 1"));Promise.resolve().then(() => console.log("tick 2"));
Real job outputpending
inputPlain value

Choose an input, then run the visible promise code.

Try it yourself

Plain value: Run the experiment. The output is computed by real promise jobs, then reset returns to a plain value.

This playground runs real promises. Tick markers are small promise jobs that make the order visible; they are not a clock.

Reset restores the plain-value selection and clears the output. That makes each run independent, so you can compare the order without an earlier run leaving jobs or text behind.

What this changes in app code

Most application code should not recreate the promise resolution procedure. The useful habit is to know who owns each promise. A function can return a request promise to its caller, or it can attach a failure handler at the boundary that can show a message, retry, or recover.

Give a UI order a clear failure ownerPop out in the code editor (opens in a new tab)JavaScript
function showOrder(order) {  return order.then((food) => console.log("show", food));} showOrder(Promise.resolve("burger")).catch((reason) => console.log("problem", reason));

Line 1 names the boundary that chooses what successful food display means. Line 2 returns the chained promise, so a later failure can still be owned. Line 5 attaches that owner. Returning promises is often clearer than starting a chain and forgetting where failure should go.

Saved reaction, queued job, or host tracking?
  • A then handler added to a pending order
  • A then handler added after fulfillment
  • Two waiting handlers after fulfill("burger")
  • Adopting { then(resolve) { resolve("burger"); } }
  • A rejected browser promise still has no handler
  • A rejected Node promise stays unhandled
Try it yourself
0 of 6 correct

Sort each card by the stage it describes. Read the explanation after every placement.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Resolve runs handlers now.” It starts resolution; reactions run as later jobs.
  • “A settled promise makes `then` synchronous.” Its new reaction still queues work.
  • “Every thenable is a Promise.” A thenable only needs a callable then.
  • “An unhandled report handles the error.” It only tells you that your code did not handle it in time.
  • “Extra ticks mean a timer.” The lesson's extra turns are promise jobs, not timer tasks.
Similar promise words, separated
WordWhat it meansDo not confuse it with
resolveBegins resolving a promiseCalling every later then handler immediately
Settled promiseAlready fulfilled or rejectedA promise whose handlers run synchronously
ThenableObject or function with callable thenAlways the same object as a native Promise
Unhandled reportHost notification after a turn without a handlerA replacement for adding .catch in app code

When an ordering surprise appears, write the synchronous statements first. Then list the promise jobs that were created. This simple trace is more useful than guessing that a browser, network request, or timer changed the order.

It also keeps the vocabulary honest. A reaction is saved information. A job is runnable work. A host report is outside the promise's success or failure value. Calling all three things “callbacks” hides the exact step that made an output move.

Practice exercises

Exercise 1 · Warm-upPredict the first log

Type the first printed word.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const order = Promise.resolve("burger");
order.then((food) => console.log(food));
console.log("waiting");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upKeep two reactions in order

    Which person receives the burger first?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const order = Promise.resolve("tea");
    order.then((food) => console.log("Asha", food));
    order.then((food) => console.log("Noah", food));
    console.log("waiting");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName the adoption job

      A checkout library returns a thenable rather than a native promise. Name the job that adopts it.

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeGive failure an owner

        What should this chain add to handle a sold-out order?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const order = Promise.reject("sold out");
        order.catch((reason) => console.log("handled", reason));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeApply it to an order screen

          A real order screen calls this function. Where should it handle a failed request?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function showOrder(order) {
            order.then((food) => console.log("show", food));
          }
          showOrder(Promise.resolve("burger"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeRead the tick probe

            How many extra ticks does the adopted native-promise case take in this lesson's probe?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Use the burger-order model, then verify each answer with state, reaction, job, and host-tracking vocabulary.

              Promise internals quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Where does a pending promise keep a then handler?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the first order snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const order = Promise.resolve("burger");
                order.then((food) => console.log(food));
                console.log("waiting");

                Choose an answer to see the explanation.

              3. Question 3 of 7What job is associated with a ready reaction?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why can resolving with a thenable take more turns?

                Choose an answer to see the explanation.

              5. Question 5 of 7In the ordering probe, how much later does resolve(Promise.resolve(value)) arrive?

                Choose an answer to see the explanation.

              6. Question 6 of 7What can a browser report for a rejection with no handler?

                Choose an answer to see the explanation.

              7. Question 7 of 7What should application code do with a request promise?

                Choose an answer to see the explanation.

              Key takeaways

              • A pending promise stores reaction records from then and catch.
              • Settlement turns ready reactions into PromiseReactionJobs, so handlers run after synchronous code.
              • Thenable adoption uses PromiseResolveThenableJob, which can add observable microtask turns.
              • Resolving with another promise can arrive later than resolving with a plain value.
              • Browsers and Node track rejected promises that remain unhandled and report them through host events.
              • Real code still needs a clear owner for every failure.

              Remember the one-liner.
              A promise saves reactions now and runs them later as jobs.

              Coming next: await under the hood, where an async function pauses and resumes using this promise machinery.

              CompleteFrontend Clear concepts. Working examples.