completefrontend
Open lab My progress
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
    Follow a pending promiseExplain how then saves a handler on a waiting list, and why settling the promise puts handlers in a job line instead of calling them at once.
  • 02
    Spot the extra jobsPredict when resolving with a promise or a thenable needs a PromiseResolveThenableJob, so the handler runs a few jobs later.
  • 03
    Handle rejections on purposeExplain what a browser and Node report when a rejected promise still has no handler after the waiting jobs have run.

A promise keeps a waiting list

A promise is an object that stands for a result that may not be ready yet. In your code, you make one, call then or catch on it, and wait. Inside, the promise keeps track of three things:

  • its state: still waiting, done, or failed;
  • its result: the value, or the reason it failed;
  • a waiting list of functions that want the result.
Definition

A promise reaction is what then or catch saves on the promise: your function, plus a note of where its result should go. It never runs in the middle of the code that added it. It runs later, as a job: a small piece of work that waits in line until the code running now has finished.

A ready order still prints secondPop 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 makes a promise called order that already holds "burger". Line 2 adds a handler: a function that runs once the promise has its result. This one prints the food. Line 3 prints waiting.

The output is waiting, then burger. The handler on line 2 waited, even though the burger was already there.

Line 3, log waiting, goes straight to the console. The handler from line 2, log food, waits in a box called jobs, and prints burger after.line 3 runs nowlog("waiting")line 2 waits herelog(food)jobsConsolewaitingburger
The handler from line 2 does not run at once. It waits in the jobs line. Line 3 prints waiting first, and then the waiting job prints burger.

You learned how to use promises in Promises, Promise chaining, Microtasks, and Build a promise. This lesson uses the same food-order examples and shows what happens inside the promise.

States and saved handlers

A promise is always in one of three states:

  • Pending: there is no result yet.
  • Fulfilled: it has a value, like "burger".
  • Rejected: it failed, and it has a reason, like "sold out".

Every promise starts pending. It settles only once: it becomes fulfilled or rejected. After that, its state never changes again.

The next code is not the real Promise. It is a small model made for this lesson, so you can see the waiting list. It has three helpers:

  • makeOrder() makes a pending order. Its then adds a handler to the waiting list.
  • order.fulfill(value) gives the order its value.
  • runJobs() runs every job in the line, first to last.
Two people wait for 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 makes a pending order. Lines 3 and 4 add two handlers, one for Asha and one for Noah. The order has no food yet, so it saves both on its waiting list.

Line 6 fulfills the order with "burger". This does not run the handlers. It moves them into the line of jobs, Asha first. Line 7 runs the jobs. The output is Asha gets burger, then Noah gets burger.

Left: before line 6, the pending order holds two waiting handlers, Asha and Noah. Right: after order.fulfill burger, both handlers move into a box called jobs, Asha first, then Noah.order.then(...)AshaNoahorderpending: both waitorder.fulfill("burger")AshaNoahjobsAsha runs first
While the order is pending, each then adds a handler to its waiting list. Line 6 fulfills the order. Both handlers move into the jobs line in the order they joined, so Asha runs before Noah.
Real-life analogyA fast-food order token

At a fast-food counter, you get a token number. Your friends wait for the same order. When the food is ready, the staff call each person in the order they joined. The food gets ready once. It does not get ready a second time.

In real life: Your token number is on the screen, not ready yet
In JavaScript: A promise is pending
In real life: Several people wait for the same order
In JavaScript: Each then saves one handler
In real life: Staff call people in the order they joined
In JavaScript: Handlers run in the order they were added
In real life: The food gets ready only once
In JavaScript: A promise settles only once

In the real specification, a saved handler is called a reaction record. It holds two things:

  • the function you passed to then;
  • the new promise that then gave back. That promise gets the function's result.

The model keeps only the function, because that is all this lesson needs.

Reaction jobs run the handlers later

When a saved handler is ready to run, JavaScript makes a PromiseReactionJob. That is the specification's name for "the job that runs one saved handler". It happens in two cases:

  • A pending promise settles. Each handler on its waiting list becomes one job.
  • You call then on a promise that has already settled. The new handler becomes a job straight away.

Either way, the job waits in line. Promise jobs wait in a line called the microtask queue. That line starts as soon as the code running now has finished.

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 makes an order that already holds "burger". Line 3 adds a handler. The order is ready, but the handler is not called here. It goes into the job line. Line 4 prints straight away. The output is after then, then first burger.

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

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

Running in
  1. script
Next: line 1
 order.then((food) => console.log("Asha gets", food));order.then((food) => console.log("Noah gets", food)); order.fulfill("burger");runJobs();
CallStoreChangeResultBlue line: runs next. 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 step-through above replays the small model from the last section, one step at a time. It is a replay of the lesson's model, not a look inside a real JavaScript engine:

  1. Lines 3 and 4 save Asha, then Noah.
  2. Line 6 settles the order and fills the job line.
  3. Line 7 runs the line, so Asha comes before Noah.

Why wait at all? The code that settles a promise can finish its own work first. A handler can do many things: start more promise work, return another promise, or throw an error. None of that can break into the middle of the line that settled the promise.

Handlers run in the order they were added. That order only counts among jobs in the same line. It does not mean every browser event waits behind them. A later lesson on the microtask queue shows where these jobs sit next to timers and screen updates.

Thenables get one extra job

A thenable is any object (or function) that has a then method you can call. From the outside it looks like a promise, but it may not be a real one. Its then method is ordinary code that someone wrote. It might throw an error, or call back twice.

When you resolve a promise with a thenable, the promise does not keep the thenable as its value. It adopts it: it calls the thenable's then and takes the result that comes back. To do that safely, JavaScript queues a separate job called PromiseResolveThenableJob.

A small model of an order taking a thenable's resultJavaScript
const order = makeOrder();const kitchenToken = {  then(resolve) {    resolve("burger");  },}; resolveOrder(order, kitchenToken);runJobs();

Line 1 makes a pending order. Lines 2 to 6 make a plain object, kitchenToken, with a then method. That makes it a thenable. Its then calls resolve("burger") on line 4.

Line 8 resolves the order with kitchenToken. The model does not call then right here. It queues a PromiseResolveThenableJob. Line 9 runs the jobs. Only then does the order get "burger".

Line 8, resolveOrder with kitchenToken, puts a job called call then into the jobs box. Line 9 runs that job, and the order box ends up holding burger.resolveOrder(order, kitchenToken)call then()jobsline 8 adds this job"burger"orderline 9: order gets burger
Line 8 does not call kitchenToken.then straight away. It adds one extra job to the jobs line. Line 9 runs that job, and only then does the order get "burger".
Step through an order taking a thenable's result
Step 0 of 6Ready
Your turn: follow the blue line

Replay the thenable model. The extra job explains why a promise-like value can make a handler run later.

Running in
  1. script
Next: line 1
const kitchenToken = {  then(resolve) {    resolve("burger");  },}; resolveOrder(order, kitchenToken);runJobs();
CallStoreChangeResultBlue line: runs next. 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.

Why a separate job? A thenable can throw an error, call resolve twice, or come from code you don't control. The rules for adopting a thenable deal with each of those cases. The model leaves those rules out. It keeps the main point: taking a thenable's result costs one extra job.

Real promises have a then method too, so they follow the same rules. That is why a library can hand you a promise-like object, and you can still call then on it. The cost is a few more jobs than for a plain string.

When your output comes in a surprising order, first ask: what did I resolve with?

  • A plain value, like a string.
  • A real promise.
  • Some other object with a then method.

The answer often explains why a handler ran a little later.

Why a promise inside a promise arrives later

Resolving with a plain value and resolving with another promise look alike when you write them. Inside, they are different. With another promise, the outer promise must first follow the inner one. That takes extra jobs before the outer handler can run.

A tick here means one turn of the job line: one waiting job runs. To make the ticks visible, the next code adds two marker jobs. They are tiny promise jobs that only print tick 1 and tick 2.

A plain value and a promise, side by sidePop 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));}
  1. Line 1 makes a promise called later that already holds "tea".
  2. Line 2 makes valueOrder and resolves it with the plain string "value".
  3. Line 3 makes promiseOrder and resolves it with the promise later.
  4. Lines 5 and 6 add a handler to each order.
  5. Lines 8 to 10 add the two marker jobs.
The console prints value arrives, tick 1, tick 2, then promise arrives. The plain value is ready at once. The promise case comes after both marker jobs.Consolevalue arrivestick 1tick 2promise arrivesplain value: ready at oncemarker jobspromise: after both markers
The real output, top to bottom. The plain value arrives first. The order that got a promise arrives only after both marker jobs, because it must first follow later.

The output is value arrives, tick 1, tick 2, promise arrives. valueOrder was fulfilled during line 2, so its handler was ready first. promiseOrder had to follow later first. That took two extra jobs, so both markers ran before it.

Read this as an order, not a time. The markers make one fact easy to see in this exact code. The lesson test checks this printed order. It does not promise the same number of ticks in every engine or every app.

Both results still come later than the code running now. The only difference is how many jobs run first.

To check your own code, add marker jobs like these and compare the whole output. Markers are easier to reason about than milliseconds, which change when the computer is busy.

The table below lists the four kinds of promise work in this lesson. Read each row as: what it is, when it shows up, and what it does.

Four kinds of promise work, side by side
What it isWhen it shows upWhat it does
A reaction record (a saved handler)When then is called on a pending promise.It holds your function and the new promise that gets its result.
A PromiseReactionJobWhen a saved handler is ready to run.It runs the handler from the job line, after the code running now.
A PromiseResolveThenableJobWhen a promise is resolved with a thenable.It calls the thenable's then safely, so the promise can take its result.
A host rejection reportWhen a rejected promise still has no handler after the waiting jobs.The browser fires an event on window, or Node fires one on process.

Reports for rejections nobody handles

A rejected promise needs a handler, such as catch, that decides what the failure means. Sometimes a promise is rejected, and it still has no handler after the waiting jobs have run. Then the host notices. The host is the program that runs your JavaScript: a browser or Node. This is called unhandled rejection tracking.

The JavaScript language says how promises behave. The host decides how to tell you about a rejection that nobody handled:

  • A browser fires an unhandledrejection event on window.
  • Node fires an unhandledRejection event on process.
Listen for unhandled rejections in a browserPop 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")));
  1. Lines 1 to 4 add a listener for the browser's unhandledrejection event. On line 3, event.preventDefault() stops the browser from printing its own error.
  2. Line 6 makes an order that is already rejected with "sold out".
  3. Line 7 adds a catch handler a moment later. queueMicrotask(fn) puts fn in the job line, so the handler is added by a job.

The handler arrives before the browser checks for unhandled rejections. So the code shows the listener and a late handler, without leaving a real unhandled error in the editor.

Node reports a rejection nobody handledJavaScript
process.on("unhandledRejection", (reason) => {  console.log("unhandled", reason);}); Promise.reject("sold out");setTimeout(() => console.log("done"), 0);

Line 1 adds a listener for Node's unhandledRejection event. Line 5 makes a promise rejected with "sold out" and never handles it. Line 6 sets a timer that prints done.

The lesson test runs this code in a separate Node program. It prints unhandled sold out, then done. In real code, add the handler yourself. Don't count on the host's report.

A browser can also fire rejectionhandled. That happens when a promise was already reported, and a handler is added later. The event helps you find bugs. It is not a reason to add handlers late. A request should have a clear plan for failure before the user sees its result.

Node and browsers show these reports in different ways, and Node has settings that change them. So the lesson only checks the listener's output. The rule you control is simpler: return the promise to code that handles it, or add the handler yourself.

Try three kinds of input

The playground resolves a new promise with the input you pick. Pick one at a time and run it:

  • Plain value: the string "burger". No extra jobs are needed.
  • Real promise: a promise made by Promise.resolve that holds the burger. The outer promise must follow it.
  • Thenable object: an object with a then method. The outer promise must follow it too.

The playground runs real promises. It prints the two marker jobs and the moment the food arrives, in the order they happen.

Playground: pick what resolve gets
The input you picked goes to resolveJavaScript
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"));
What the real promises printedpending
inputPlain value

Pick an input, then press Run promises.

Try it yourself

Plain value: press Run promises to see when the food arrives. This output comes from real promise jobs. Reset goes back to Plain value.

This playground runs real promises. Each tick line is a small marker job that shows the order. Ticks are not a clock.

Reset picks Plain value again and clears the output. So every run starts fresh, and an earlier run can't leave jobs or text behind.

What this changes in your code

Most app code never rebuilds these rules. The useful habit is to know who owns each promise. The owner is the code that decides what happens when the promise fails. A function can do one of two things:

  • return the promise, so the code that called it can handle the failure;
  • add a catch handler itself, at a place that can show a message, try again, or recover.
Return the promise so the caller can handle failurePop 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 starts showOrder, which decides how to show the food. Line 2 returns the promise chain, so whoever called showOrder can still handle a failure. Line 5 calls showOrder and adds catch. That catch is the owner of any failure. Here nothing fails, so the code prints show burger.

Returning promises is usually clearer than starting a chain and forgetting where failures should go.

Saved handler, waiting job, or host report?
  • A then handler added to a pending order
  • A then handler added after the order is fulfilled
  • Two saved handlers after fulfill("burger")
  • Taking the result of { then(resolve) { resolve("burger"); } }
  • A rejected promise in a browser still has no handler
  • A rejected promise in Node is never handled
Try it yourself
0 of 6 correct

Put each card in the group it belongs to. Read the explanation after each one.

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

Common misconceptions

  • "resolve runs the handlers right away." It only starts the process. The handlers run later, as jobs.
  • "then on a settled promise runs at once." The new handler still waits in the job line.
  • "Every thenable is a real promise." A thenable only needs a then method you can call.
  • "The unhandled report handles the error." It only tells you that your code did not handle it in time.
  • "Extra ticks mean a timer." The extra turns in this lesson are promise jobs, not timers.

Each row of the table takes one word from this lesson. Read it as: what the word really means, and what it does not mean.

Promise words that are easy to mix up
The wordWhat it meansWhat it does not mean
resolveIt starts giving the promise its result.It does not call every then handler right away.
A settled promiseIt is already fulfilled or rejected.Its new handlers do not run at once; they still wait in the job line.
A thenableAny object or function with a then method you can call.It is not always a real Promise.
An unhandled reportA message from the host when a rejection still has no handler.It does not replace adding .catch in your code.

When the output order surprises you, first write down the lines that run straight away. Then list the promise jobs, in the order they were made. This short list usually explains the order. Don't guess that the browser, the network, or a timer changed it.

It also keeps the words clear:

  • A reaction is a saved handler.
  • A job is work waiting in line to run.
  • A host report is a message from the browser or Node. It is not the promise's value or reason.

If you call all three "callbacks", you can't tell which step moved your output.

Practice exercises

Exercise 1 · Warm-upPredict the first log

Type the first word this code prints.

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 handlers in order

    Who gets the tea 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 extra job

      A checkout library gives you a thenable, not a real promise. You resolve a promise with it. What is the name of the job that takes the thenable's result?

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

        Exercise 4 · PracticeGive the failure an owner

        This order is rejected with "sold out". Which promise method handles it?

        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 · ChallengeUse it on 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 markers

            In this lesson's tick code, how many marker jobs run before the promise case arrives?

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

              Check your understanding

              Use the food-order examples. For each answer, ask: is this a saved handler, a job waiting in line, an extra job for a thenable, or a report from the host?

              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 this code 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 7Which job runs a saved handler once it is ready?

                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 lesson's tick code, when does the order resolved with a promise arrive?

                Choose an answer to see the explanation.

              6. Question 6 of 7What can a browser fire for a rejection that has no handler?

                Choose an answer to see the explanation.

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

                Choose an answer to see the explanation.

              Key takeaways

              • A pending promise saves the handlers from then and catch on a waiting list.
              • When it settles, each saved handler becomes a job, called a PromiseReactionJob. So handlers run after the code running now.
              • Taking a thenable's result needs one more job, called a PromiseResolveThenableJob. That can add extra ticks.
              • Resolving with another promise can arrive later than resolving with a plain value.
              • Browsers and Node report rejected promises that nobody handled, through events.
              • Your code still needs a clear owner for every failure.

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

              Coming next: await under the hood. You will see how an async function pauses and starts again, using these same promise jobs.

              CompleteFrontend Clear concepts. Working examples.