completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Build a promise from scratch

Build a MyPromise class with states, then chaining, microtask scheduling, thenable adoption, combinators, and Promises/A+ context.

By the end, you can
  • 01
    Build the core of a promiseStore a pending, fulfilled, or rejected state, keep a list of handlers, and settle only once.
  • 02
    Make then chain correctlyReturn a new promise from then, adopt a returned promise or thenable, and pass thrown errors on.
  • 03
    Compare with the built-in PromiseExplain when handlers run, how combinators work, what Promises/A+ covers, and what the built-in Promise adds.

Build a promise to see how it works

Earlier lessons showed how to use promises: their states, chains, combinators, errors, microtasks, and the event loop. In this lesson you build your own small promise class, called MyPromise. Building one shows you why promises behave the way they do.

Plain definition

A promise is an object that holds one result that will arrive later. To build one, you need five parts:

  • A state: pending, fulfilled, or rejected. It changes only once.
  • A result: the value, or the reason it failed.
  • A list of handlers: the functions given to then, waiting for the result.
  • A then method that returns a new promise for each handler's outcome.
  • A rule that handlers always run as microtasks: small jobs that run right after the current code finishes, never right away.

You build MyPromise in five versions. Each version adds one rule. Read each row of the table as: "in this version, the class learns this rule, and this is why it matters."

The five versions of MyPromise
VersionWhat the class learnsWhy it matters
1. StatesIt stores pending, fulfilled, or rejected, keeps one result, and settles only once.An error thrown in the executor becomes the rejection reason.
2. Handlersthen saves handlers while the promise is pending, and schedules them once it settles.A handler added late still gets the stored result.
3. Chainingthen returns a new promise and settles it from what the handler returns.A missing handler passes the value or reason on.
4. ResolutionA returned promise or thenable is adopted, and only its first call counts.A promise resolved with itself rejects with a TypeError.
5. MicrotasksHandlers run through queueMicrotask, never right away.The run order matches the built-in Promise in every tested case.
Real-life analogyYour order token at a fast-food counter

You order, pay, and get a token with your order number. While the food cooks, people can wait for it. The counter marks the order ready or sold out one time. Then it tells everyone who is waiting.

In real life: Your token says your food is still cooking
In JavaScript: The pending state
In real life: The counter marks the order ready or sold out, once
In JavaScript: fulfilled or rejected, guarded so it happens once
In real life: People wait to hear about your token
In JavaScript: then handlers wait in a list
In real life: The counter tells them after it marks the order
In JavaScript: Handlers run later, as microtasks

In real apps you will still use the built-in Promise. Building one yourself is practice in reading code. Once you have seen every part, you can explain a promise bug step by step.

Version 1: states, result, and executor

CORE STATE

Start with the smallest class that works. A new promise starts pending: it has no result yet. Later it settles, which means it gets its final result. It can settle in one of two ways:

  • Fulfilled, with a value. The resolve function does this.
  • Rejected, with a reason. The reject function does this.

Both functions start with the same check, called a guard: if the state is no longer pending, stop right away. This guard is why extra calls to resolve and reject do nothing.

The executor is the function you pass to new MyPromise(...). The class calls it right away and hands it resolve and reject.

Version 1: state, result, and executorJavaScript
class MyPromise {  constructor(executor) {    this.state = "pending";    this.result = undefined;     const resolve = (value) => {      if (this.state !== "pending") return;      this.state = "fulfilled";      this.result = value;    };    const reject = (reason) => {      if (this.state !== "pending") return;      this.state = "rejected";      this.result = reason;    };     try {      executor(resolve, reject);    } catch (error) {      reject(error);    }  }}

Line 3 stores the state. Lines 7 and 12 are the guards. Line 18 calls the executor. If the executor throws an error, line 20 catches it and rejects the promise. This version can't run handlers yet. But it already has the most important rule: a promise gets one outcome.

A card that says pending, the starting value of this.state. One arrow, labeled resolve(value), leads to a card that says fulfilled, which keeps a value. Another arrow, labeled reject(reason), leads to a card that says rejected, which keeps a reason.pendingthis.stateresolve(value)fulfilledkeeps a valuereject(reason)rejectedkeeps a reason
this.state starts as "pending". The first call to resolve or reject moves it once; the guards on lines 7 and 12 ignore every later call.
When the executor runs

The executor runs right away, during new MyPromise(...). It starts the work. Handlers added with then run later, after the current code finishes.

The executor runs before the handlerPop out in the code editor (opens in a new tab)JavaScript
const order = [];const ticket = new MyPromise((resolve) => {  order.push("executor");  resolve("ready");});order.push("after new");ticket.then((value) => console.log(order.join(" -> ") + ": " + value));

Line 2 creates the promise, so line 3 runs right away and adds "executor" to the order array. Line 4 fulfills the promise with "ready". Line 6 still runs before the handler. Then the handler from line 7 prints executor -> after new: ready.

A throw after resolve changes nothingPop out in the code editor (opens in a new tab)JavaScript
const saved = new MyPromise((resolve) => {  resolve("saved");  throw new Error("late");});saved.then((value) => console.log(value));const failed = new MyPromise(() => { throw new Error("offline"); });failed.catch((error) => console.log("caught: " + error.message));const adopted = new MyPromise((resolve) => {  resolve({ then: (fulfill) => setTimeout(() => fulfill("adopted"), 0) });  throw new Error("after resolve");});adopted.then((value) => console.log("adopted: " + value));

This example has three executors:

  • The first calls resolve on line 2, then throws on line 3. The promise already has its value, so the throw can't replace "saved". Line 5 prints saved.
  • The second throws on line 6 while the promise is still pending. So the promise rejects, and line 7 prints caught: offline.
  • The third calls resolve on line 9 with a thenable: an object with a then method. Version 4 explains thenables. The promise is still waiting for the thenable when line 10 throws. But calling resolve already locked the promise, so the throw is ignored. Line 12 prints adopted: adopted.

Version 2: handlers and then

STEP THROUGH

A promise is useful when other code can react to its result. A handler is a function you pass to then. In this version, then does one of two things:

  • If the promise is pending, it saves the handler in a list.
  • If the promise has settled, it schedules the handler to run soon, with the stored result.
Version 2: save handlers, settle once
Step 0 of 9Ready
Your turn: follow the blue line

A promise keeps a state, a result, and a list of handlers. Step through a pending promise that settles once.

Running in
  1. script
Next: line 1
const ticket = new MyPromise((resolve) => {  release = resolve;}); ticket.then((value) => {  console.log("first: " + value);});ticket.then((value) => {  console.log("second: " + value);}); release("order ready");release("ignored");
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.

Both handlers get "order ready". Running a handler doesn't use up the value. A promise is a stored result plus a list of handlers waiting for it. It is not like an event, which is gone once its listeners have run.

A cart called handlers holds two function cards, first and second, saved by lines 6 and 9 while the promise is pending. An arrow for line 13 leads to a console window that prints first: order ready, then second: order ready.lines 6 and 9 save thesefirstsecondhandlersline 13settlesConsolefirst: order readysecond: order ready
While the promise is pending, lines 6 and 9 save both handlers in a list. Line 13 settles it, and both handlers get the same value.
The same handler-list examplePop out in the code editor (opens in a new tab)JavaScript
let release;const ticket = new MyPromise((resolve) => {  release = resolve;}); ticket.then((value) => {  console.log("first: " + value);});ticket.then((value) => {  console.log("second: " + value);}); release("order ready");release("ignored");
Add a rejection handler after the promise rejectsPop out in the code editor (opens in a new tab)JavaScript
const request = MyPromise.reject("offline");request.then(null, (reason) => console.log("handled later: " + reason));

Line 1 rejects the promise before any handler is added. Line 2 adds a handler later. It still gets the stored reason in a microtask, and prints handled later: offline.

An unhandled rejection is a rejection that no handler ever catches. Browsers and Node.js warn you about those. MyPromise keeps the rejection for later handlers, but it never sends that warning.

Version 3: then returns a new promise

CHAINING

The built-in then never changes the promise you call it on. It returns a new promise right away. Later, when the handler runs, the new promise settles from what the handler does:

  • The handler returns a value: the new promise fulfills with that value.
  • The handler throws an error: the new promise rejects with that error.
  • The handler returns a promise: the new promise waits for it and takes its result. This is called adopting the promise.
Version 3: each then returns a new promise
Step 0 of 5Ready
Your turn: follow the blue line

Every then returns a new promise. Follow what each handler returns, and what the next promise adopts.

Running in
  1. script
Next: line 1
  .then((value) => {    return value + 3;  })  .then((value) => {    return MyPromise.resolve(value * 2);  })  .then((value) => {    console.log(value);  });
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.

Read the chain one handler at a time. Line 3 returns a plain number, 5, so the next promise fulfills with 5. Line 6 returns another promise, so the next promise adopts it. Line 9 gets the adopted value, 10, not a promise object.

Three boxes in a row, one promise each. The start box holds 2. An arrow labeled plus 3 leads to the then 1 box, which holds 5. An arrow labeled times 2 leads to the then 2 box, which holds 10, adopted from the promise that line 6 returns.2startline 1: 2+ 35then 1line 3: returns 5× 210then 2line 6: adopts 10
Each then gives back a new promise. Line 3 returns 5. Line 6 returns a promise for 10, so the next promise adopts it, and line 9 logs 10.
The chain on its ownPop out in the code editor (opens in a new tab)JavaScript
MyPromise.resolve(2)  .then((value) => {    return value + 3;  })  .then((value) => {    return MyPromise.resolve(value * 2);  })  .then((value) => {    console.log(value);  });
Values and errors move on to the next promisePop out in the code editor (opens in a new tab)JavaScript
const original = MyPromise.reject("offline");const next = original.then();console.log("new promise: " + (next !== original));next.catch((reason) => console.log("passed through: " + reason));MyPromise.resolve("tea").then(() => { throw new Error("no tea"); })  .catch((error) => console.log("caught: " + error.message));

Line 1 creates a rejected promise. Line 2 calls then with no handlers at all. Line 3 checks that then returned a different promise, and prints new promise: true. Line 4 catches the same "offline" reason, because nothing handled it on the way. Line 5 throws inside a handler. Line 6 catches that new error and prints caught: no tea.

So then must also deal with missing handlers. The first function you pass to then is the fulfillment handler; it runs on success. The second is the rejection handler; it runs on failure.

  • No fulfillment handler: the value passes through to the next promise.
  • No rejection handler: the reason passes through, so the next promise rejects too.
  • A handler that throws: the next promise rejects with that error.

These rules are all that catch needs. You will build it from then later in this lesson.

Version 4: the Promise Resolution Procedure

THENABLES

Storing a value is easy. The hard part is deciding what to do with the value a handler returns. The rules for this have a name: the Promise Resolution Procedure. They say:

  • If the value is the promise itself, reject with a TypeError. A promise can't wait for itself.
  • If the value is a plain value, like a number, fulfill with it.
  • If the value is a thenable, read its then once, call it, and adopt the result. Every promise is a thenable, and so is any object with a then method.
  • Only the first call to resolve or reject from a thenable counts.
Version 4: adopt a thenable, and only its first call counts
Step 0 of 7Ready
Your turn: follow the blue line

The resolution rules are the heart of chaining: adopt promises and thenables, reject a promise resolved with itself, and count only the first call.

Running in
  1. script
Next: line 1
  then(resolve, reject) {    resolve("first");    reject("second");    resolve("third");  },}; MyPromise.resolve(thenable)  .then((value) => value + " adopted")  .then((value) => console.log(value));
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.
Three calls from the thenable, in order. resolve first, on line 3, is used. reject second, on line 4, and resolve third, on line 5, are ignored. An arrow from the first call leads to a console window that prints first adopted.line 3: usedresolve("first")reject("second")line 4: ignoredresolve("third")line 5: ignoredConsolefirst adopted
The thenable makes three calls, but only the first one counts. The chain adopts "first", and line 10 turns it into "first adopted".
Fulfill, adopt, or reject?
  • handler returns `42`
  • handler returns `Promise.resolve("ok")`
  • handler returns `{ then(resolve) { resolve("ok"); } }`
  • handler throws `new Error("boom")`
  • resolve a promise with itself
  • handler returns `{ value: 1 }`
Try it yourself
0 of 6 correct

Sort each handler result by what happens to the promise that then returned.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Read then only oncePop out in the code editor (opens in a new tab)JavaScript
let reads = 0;const order = {  get then() { reads += 1; return (resolve) => resolve("tea"); },};MyPromise.resolve(order).then((value) => console.log(value + ": " + reads));

A getter is a property that runs a function each time you read it. Line 1 starts a counter at 0. Line 3 makes then a getter: each read adds 1 to the counter and returns a function. MyPromise reads then once, keeps the function, and calls it. So line 5 prints tea: 1. If the code read then twice, a getter like this could give two different answers.

A then getter that throws becomes a rejectionPop out in the code editor (opens in a new tab)JavaScript
const order = {  get then() { throw new Error("broken order"); },};MyPromise.resolve(order).catch((error) => console.log("rejected: " + error.message));

Line 2 makes then a getter that throws an error. The procedure catches the error and rejects the promise. So the catch on line 4 prints rejected: broken order. A broken thenable can't crash the chain. Its error goes down the normal rejection path instead.

A promise resolved with itselfPop out in the code editor (opens in a new tab)JavaScript
let nativeResolve;const native = new Promise((resolve) => {  nativeResolve = resolve;});nativeResolve(native);native.catch((error) => {  console.log("native: " + error.name + ": " + error.message);}); let myResolve;const mine = new MyPromise((resolve) => {  myResolve = resolve;});myResolve(mine);mine.catch((error) => {  console.log("mine: " + error.name + ": " + error.message);});

Line 5 resolves the built-in promise with itself. Line 14 does the same with MyPromise. Both reject with a TypeError, and lines 7 and 16 print the messages.

  • In Node 22 and Chrome, the built-in message is TypeError: Chaining cycle detected for promise #<Promise>.
  • MyPromise uses the same message, but names #<MyPromise>.

Promises/A+, the shared rulebook you'll meet near the end of this lesson, requires the TypeError. It doesn't require the exact words.

Version 5: run handlers as microtasks

ORDER PROOF

A handler must never run right away, inside then. Not even when the promise is already fulfilled. It must wait until the code that is running now has finished. MyPromise does this with queueMicrotask, a built-in function that runs a function as a microtask. In very old environments without queueMicrotask, it uses a built-in promise to do the same job.

MyPromise and the built-in Promise run in the same orderPop out in the code editor (opens in a new tab)JavaScript
console.log("sync");MyPromise.resolve("mine").then((value) => console.log("my " + value));Promise.resolve("native").then((value) => console.log("native " + value));queueMicrotask(() => console.log("queued"));setTimeout(() => console.log("timeout"), 0);

The output is sync, my mine, native native, queued, then timeout. Line by line:

  • Line 1 runs right away.
  • The MyPromise handler runs before the built-in one, because line 2 adds it first.
  • Both promise handlers and the job from line 4 are microtasks.
  • The timer on line 5 is a task, a bigger job. Every waiting microtask runs before the next task, so timeout prints last.
Where this builds on earlier lessons

The Microtasks in depth lesson explains the microtask queue. The Event loop lesson explains why the timer waits. Here you need one rule: schedule handlers, never call them right away.

catch, finally, resolve, and reject

SMALL WRAPPERS

Once then and the resolution rules work, the other methods are short:

  • catch(onRejected) is just then(null, onRejected).
  • finally(cleanup) runs cleanup, then passes on the first value or reason. If cleanup fails, the promise rejects with that error instead.
  • MyPromise.resolve(value) makes a promise fulfilled with value, or adopts it if it is a thenable. If value is already a MyPromise, it gives it back as it is.
  • MyPromise.reject(reason) makes a promise rejected with reason.
The four small methods in usePop out in the code editor (opens in a new tab)JavaScript
MyPromise.resolve("tea").then((value) => {  console.log("value: " + value);}); MyPromise.reject("missing").catch((reason) => {  console.log("caught: " + reason);}); MyPromise.resolve("saved")  .finally(() => {    return MyPromise.resolve("cleanup").then((value) => console.log(value));  })  .then((value) => console.log("after finally: " + value));

Lines 1 to 3 print value: tea. Lines 5 to 7 print caught: missing. On line 11 the cleanup returns a promise, which prints cleanup. finally wraps it in MyPromise.resolve(cleanup()).then(...), so it waits for that promise. Only then does line 13 get "saved" and print after finally: saved.

Combinators: many promises, one result

ALL & RACE

A combinator is a method that takes many promises and gives back one promise. You call it on the class, like MyPromise.all(...), not on one promise. Each combinator is a loop that calls MyPromise.resolve and then on every input. It needs no hidden engine features. It only needs careful counting:

  • Keep the results in the same order as the inputs.
  • Settle only once.
  • Treat a plain value, like "plain", as an already-fulfilled promise.
Promise.all keeps the input orderPop out in the code editor (opens in a new tab)JavaScript
const slow = new MyPromise((resolve) => {  setTimeout(() => resolve("slow"), 20);});const fast = MyPromise.resolve("fast"); MyPromise.all([slow, fast, "plain"]).then((values) => {  console.log(values.join(","));}); MyPromise.all([]).then((values) => {  console.log("empty " + values.length);});

Line 10 passes an empty array, so all fulfills with an empty array at once and prints empty 0 first. Line 6 waits for slow, fast, and "plain". slow finishes last, but its value stays first, so line 7 prints slow,fast,plain.

Promise.race takes the first promise to settlePop out in the code editor (opens in a new tab)JavaScript
const slow = new MyPromise((resolve) => {  setTimeout(() => resolve("slow"), 20);});const fast = new MyPromise((resolve) => {  setTimeout(() => resolve("fast"), 5);}); MyPromise.race([slow, fast]).then((value) => {  console.log(value);});

fast fulfills after 5 milliseconds and slow after 20. So race fulfills with "fast", and line 9 prints fast.

allSettled and anyPop out in the code editor (opens in a new tab)JavaScript
MyPromise.allSettled([  MyPromise.resolve("ok"),  MyPromise.reject("bad"),]).then((results) => {  console.log(results.map((item) => item.status).join("/"));}); MyPromise.any([  MyPromise.reject("no A"),  MyPromise.resolve("yes B"),]).then((value) => console.log(value)); MyPromise.any([  MyPromise.reject("no A"),  MyPromise.reject("no B"),]).catch((error) => {  console.log(error.name + ": " + error.errors.join(","));});
  • Lines 1 to 6 print fulfilled/rejected. allSettled waits for every input and reports each one's status.
  • Lines 8 to 11 print yes B. any uses the first input that fulfills and skips rejections.
  • Lines 13 to 18 reject with an AggregateError: an error that keeps a list of errors in its errors property. They print AggregateError: no A,no B.
Which combinator fits?
  • Load the user, their permissions, and their settings before showing the page
  • Use whichever finishes first: the request or a timeout
  • Show which of five uploads worked and which failed
  • Download from the first copy server that works
Try it yourself
0 of 4 correct

Pick the method that answers the question each feature asks about its promises.

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

Three edge cases to remember:

  • Promise.all rejects with the first rejection, but it doesn't stop the other work.
  • Promise.race([]) has nothing to wait for, so it stays pending forever.
  • Promise.any([]) rejects at once with an AggregateError, because no input can succeed.

Promises/A+ and the built-in Promise

SPEC CONTEXT

Promises/A+ is a short, shared rulebook for promises. Promise libraries wrote it before promises were built into JavaScript, so that their promises could work together. It covers two things:

  • How then must behave.
  • The Promise Resolution Procedure.

It does not cover:

  • Methods like all and race.
  • Browser warnings for unhandled rejections.
  • async and await.
  • How fast an engine runs promises.

The table compares three things. Read each row as one question, answered for MyPromise, for the built-in Promise, and for Promises/A+.

MyPromise, the built-in Promise, and Promises/A+
QuestionMyPromise in this lessonBuilt-in PromisePromises/A+
What is it?A small class for learning the core rules.A full feature of the JavaScript engine, built for speed and real apps.A shared rulebook for how then must behave.
Which methods does it have?then, catch, finally, resolve, reject, all, race, allSettled, and any.All of those, plus newer ones such as withResolvers. Newer browsers also have Promise.try.Only the then method and the Promise Resolution Procedure.
When do handlers run?As microtasks, through queueMicrotask, never right away.As jobs on the browser's or Node.js's promise queue. It also warns about unhandled rejections.After the current code finishes. It says nothing about warnings or other methods.
How is it tested?A few small tests compare order, values, rejections, thenables, and combinators.Browsers and Node.js follow the JavaScript standard for promises.The promises-aplus-tests package checks every rule. It is not installed here.
For learning, not for real apps

The built-in Promise tells the browser or Node.js when a rejection is never handled. MyPromise doesn't. It also skips other platform features and speed work. Use the built-in Promise in real code. Use MyPromise to study the rules.

MyPromise and the built-in Promise, side by side
The code both versions runJavaScript
async function runScenario(PromiseLibrary) {  const chain = await PromiseLibrary.resolve(2)    .then((value) => value + 3)    .then((value) => PromiseLibrary.resolve(value * 2));   const recovered = await PromiseLibrary.reject("lost")    .catch((reason) => "fixed " + reason);   const all = await PromiseLibrary.all([    new PromiseLibrary((resolve) => setTimeout(() => resolve("slow"), 20)),    PromiseLibrary.resolve("fast"),    "plain",  ]);   const race = await PromiseLibrary.race([    new PromiseLibrary((resolve) => setTimeout(() => resolve("slow"), 20)),    new PromiseLibrary((resolve) => setTimeout(() => resolve("fast"), 5)),  ]);   return [chain, recovered, all.join(","), race].join(" | ");}
Resultsthen chain
MyPromisePress Run comparison.
Built-in PromisePress Run comparison.
Try it yourself

Pick a case and press Run comparison. MyPromise isn't meant for real apps, but on these small cases it gives the same results as the built-in Promise.

This playground runs the fixed code on the left in your browser. You can't type your own code here.
A small test against the built-in PromisePop out in the code editor (opens in a new tab)JavaScript
const cases = [  ["value chain", (P) => P.resolve(1).then((value) => value + 1)],  ["rejection recovery", (P) => P.reject("lost").catch(() => "fixed")],  ["thenable adoption", (P) => P.resolve({ then(resolve) { resolve("thenable"); } })],  ["all order", (P) => P.all([P.resolve("A"), "B"]).then((values) => values.join("-"))],]; for (const [label, run] of cases) {  run(MyPromise).then((value) => console.log("my " + label + ": " + value));  run(Promise).then((value) => console.log("native " + label + ": " + value));}

Lines 1 to 6 list four small cases. Lines 8 to 11 run each case once with MyPromise and once with the built-in Promise, and print both results. Every pair matches.

The official promises-aplus-tests package is a full conformance suite: a large set of tests that checks an implementation against every A+ rule. It is not installed here, so this is only a small check. The built-in Promise also has features outside A+:

  • It tracks unhandled rejections.
  • Node 22 has Promise.withResolvers.
  • Newer browsers have Promise.try, but Node 22 does not.

Where this helps, and common mistakes

You will rarely ship your own promise class. But knowing how one works helps when you:

  • debug tests that use promises,
  • review helper functions built on promises,
  • wrap old callback-style functions in promises,
  • explain why a then handler didn't run when someone expected.
Use the built-in Promise in real apps

Built-in promises are faster, work with the browser and Node.js, and are tested in every engine. MyPromise is for studying, not for replacing them.

Build one to understand bugs

After you build one, common bugs have a clear cause: a missing return, a handler that runs too early, or expecting Promise.all results in finishing order.

  • “A fulfilled promise runs its handler right away.” It schedules the handler as a microtask.
  • “then changes the original promise.” It returns a new promise.
  • “Returning a promise from a handler puts a promise inside a promise.” The next promise adopts it, so you get the value.
  • “A thenable can both resolve and reject.” It can try, but only the first call counts.
  • “Promise.all stops the other promises when one fails.” It rejects early, but the other work keeps running.
  • “Promises/A+ is the whole built-in Promise.” A+ covers only then and resolution. The JavaScript standard and the browser add the rest.

Each row below is a mistake people make when they build a promise. Read it as: "if you do this, you see this, and this is the fix."

Common mistakes when building a promise
The mistakeWhat you seeHow to fix it
Handlers run right away, inside then.The output order is different from built-in promises.Always schedule handlers as microtasks.
A returned promise is treated as a plain value.The next handler gets a promise object instead of its value.Use the resolution rules to adopt thenables.
There is no one-call guard.A thenable's later call can change the outcome.Share one called flag between resolve and reject.
all stores results in finishing order.The results don't line up with the inputs.Store each result at its input's index.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the order of the logs

Type the three logs in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log("A");
MyPromise.resolve("B").then((value) => console.log(value));
console.log("C");

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

    Exercise 2 · Warm-upWrite catch with then

    Read the code and type what it prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    MyPromise.prototype.catch = function (onRejected) {
      return this.then(null, onRejected);
    };
    
    MyPromise.reject("offline")
      .catch((reason) => console.log("handled " + reason));

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

      Exercise 3 · PracticeFix a then that runs too early

      Which built-in function should schedule the handler, instead of calling it directly?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      then(onFulfilled) {
        if (this.state === "fulfilled") {
          onFulfilled(this.result); // bug: runs right away
        }
      }

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

        Exercise 4 · PracticeBuild allSettled from all

        You call this allSettled with one input that fulfills and one that rejects, then join the two statuses with /. What do you get?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        MyPromise.allSettled = function (values) {
          const input = Array.from(values);
          return MyPromise.all(input.map((value) =>
            MyPromise.resolve(value).then(
              (result) => ({ status: "fulfilled", value: result }),
              (reason) => ({ status: "rejected", reason }),
            )
          ));
        };

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

          Exercise 5 · ChallengeCollect every rejection for any

          Every input rejects. Type the line this code prints.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          MyPromise.any([
            MyPromise.reject("A"),
            MyPromise.reject("B"),
          ]).catch((error) => {
            console.log(error.name + ": " + error.errors.join("|"));
          });

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

            Check your understanding

            8 QUESTIONS
            Build a promise quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does the first version of MyPromise need?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log("A");
              MyPromise.resolve("B").then((value) => console.log(value));
              console.log("C");

              Choose an answer to see the explanation.

            3. Question 3 of 8What must then return so that chaining works?

              Choose an answer to see the explanation.

            4. Question 4 of 8What should happen when a handler passed to then is not a function?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does the Promise Resolution Procedure do with a thenable?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              MyPromise.resolve({
                then(resolve, reject) {
                  resolve("first");
                  reject("second");
                },
              }).then((value) => console.log(value));

              Choose an answer to see the explanation.

            6. Question 6 of 8What does Promise.all preserve?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const slow = new MyPromise((resolve) => setTimeout(() => resolve("slow"), 20));
              const fast = MyPromise.resolve("fast");
              MyPromise.all([slow, fast, "plain"]).then((values) => console.log(values.join(",")));

              Choose an answer to see the explanation.

            7. Question 7 of 8What does MyPromise.any reject with when every input rejects?

              Choose an answer to see the explanation.

            8. Question 8 of 8What does Promises/A+ cover?

              Choose an answer to see the explanation.

            Key takeaways

            • A promise keeps one state and one result, and it settles only once.
            • then saves handlers and returns a new promise for each handler's outcome.
            • The Promise Resolution Procedure adopts promises and thenables, rejects a promise resolved with itself, and counts only the first call.
            • Handlers always run as microtasks, even when the promise is already fulfilled.
            • Combinators are loops that call resolve and then on many promises and keep careful count.
            • Promises/A+ covers then and resolution; the built-in Promise adds more.

            One-line summary: To build a promise, let the state change only once, settle the next promise from each handler's result, and run every handler after the current code finishes.

            Up next: Classic utilities. You'll build more helpers yourself, such as deep clone, deep equal, curry, and memoize.

            CompleteFrontend Clear concepts. Working examples.