A thenable is any object or function that has a then method. Every promise is a thenable, but a thenable doesn't have to be a promise: a plain object you write can be one, and so can the promises of libraries written before JavaScript had its own Promise.
The then method takes two callbacks, usually called onFulfilled and onRejected. A thenable calls the first with its value when it succeeds, or the second with a reason when it fails. Here are two thenables that aren't promises, one that succeeds after 100 ms and one that fails:
const ready = {
then(onFulfilled, onRejected) {
setTimeout(() => onFulfilled("ready"), 100);
},
};
const broken = {
then(onFulfilled, onRejected) {
setTimeout(() => onRejected(new Error("broken")), 100);
},
};
JavaScript's promises accept a thenable wherever they accept a promise. await waits for a thenable to settle. Promise.all and the other combinators accept thenables among their inputs. And a promise that's resolved with a thenable, for example because an async function or a then callback returns one, follows it: it settles the way the thenable does.
async function load() {
return ready;
}
async function main() {
console.log(await ready); // "ready", after 100 ms
console.log(await Promise.all([ready, 1])); // ["ready", 1]
console.log(await load()); // "ready": load's promise followed the thenable
try {
await broken;
} catch (error) {
console.log(error.message); // "broken"
}
}
main();
That's how promises from different libraries work together. A library's promise needs nothing more than a then method for await and the built-in Promise to accept it, and libraries that follow Promises/A+, the common standard for promises, accept native promises for the same reason.
There's a catch: an object that has a then method for some other reason counts as a thenable too. Awaiting it, or returning it from an async function, calls its then, and if that never calls back, the promise never settles.
Write a class MyPromise that works like a simplified Promise. new MyPromise(executor) calls executor(resolve, reject) right away. The promise starts pending; the first call to resolve(value) or reject(reason) decides its outcome, and later calls do nothing. If the executor throws before that, the promise rejects with the error.
then(onFulfilled, onRejected) returns a new MyPromise. When the promise settles, the matching callback runs with the value or the reason, and the new promise resolves with what the callback returns, or rejects with what it throws. If the matching callback isn't a function, the value or reason passes through to the new promise unchanged. catch(onRejected) is then(undefined, onRejected).
Callbacks always run asynchronously, in a microtask (use queueMicrotask), even if the promise has already settled. Callbacks added to the same promise run in the order they were added.
Resolving with a thenable (any object or function with a then method, a MyPromise included), whether through resolve or by returning it from a callback, makes the promise follow it: it settles the way the thenable does.
Also add MyPromise.resolve(value), which returns value itself if it's already a MyPromise and otherwise a promise resolved with it, and MyPromise.reject(reason), which returns a promise rejected with reason. You can leave out finally, the static combinators, and the check for a promise resolved with itself.
new MyPromise((resolve) => setTimeout(() => resolve(2), 100))
.then((n) => n * 10)
.then((n) => {
throw new Error("too big: " + n);
})
.catch((error) => error.message)
.then((message) => console.log(message)); // "too big: 20"
let received;
new MyPromise((...args) => {
received = args;
});
expect(received).toBeDefined();
expect(received.map((arg) => typeof arg)).toEqual(["function", "function"]);
const log = [];
setTimeout(() => log.push("timer"), 0);
new MyPromise((resolve) => resolve("value")).then((value) => log.push(value));
log.push("sync");
await flush();
expect(log).toEqual(["sync", "value", "timer"]);
let resolve;
const promise = new MyPromise((r) => {
resolve = r;
});
const log = [];
promise.then((value) => log.push("first " + value));
promise.then((value) => log.push("second " + value));
await flush();
expect(log).toEqual([]);
resolve("done");
await flush();
expect(log).toEqual(["first done", "second done"]);
const onFulfilled = spy();
const onRejected = spy();
new MyPromise((resolve, reject) => {
resolve(1);
resolve(2);
reject(new Error("too late"));
throw new Error("also too late");
}).then(onFulfilled, onRejected);
await flush();
expect(onFulfilled).toHaveBeenCalledTimes(1);
expect(onFulfilled).toHaveBeenCalledWith(1);
expect(onRejected).not.toHaveBeenCalled();
const error = new Error("boom");
const onRejected = spy();
new MyPromise(() => {
throw error;
}).then(undefined, onRejected);
await flush();
expect(onRejected).toHaveBeenCalledTimes(1);
expect(onRejected.calls[0].args[0]).toBe(error);
const start = new MyPromise((resolve) => resolve(2));
const next = start.then((n) => n * 10);
expect(next).toBeInstanceOf(MyPromise);
expect(next).not.toBe(start);
const done = spy();
next
.then()
.then((n) => n + 1)
.then(done);
await flush();
expect(done).toHaveBeenCalledWith(21);
const error = new Error("failed");
const skipped = spy();
const done = spy();
new MyPromise((resolve) => resolve(1))
.then(() => {
throw error;
})
.then(skipped)
.catch((reason) => (reason === error ? "recovered" : "wrong reason"))
.then(done);
await flush();
expect(skipped).not.toHaveBeenCalled();
expect(done).toHaveBeenCalledWith("recovered");
const fromCallback = spy();
const fromExecutor = spy();
const onRejected = spy();
const error = new Error("inner failed");
new MyPromise((resolve) => resolve(1))
.then(() => new MyPromise((resolve) => resolve("inner")))
.then(fromCallback);
const thenable = {
then(onFulfilled) {
onFulfilled("from a thenable");
},
};
new MyPromise((resolve) => resolve(thenable)).then(fromExecutor);
new MyPromise((resolve) => resolve(1))
.then(() => new MyPromise((_, reject) => reject(error)))
.then(undefined, onRejected);
await flush();
expect(fromCallback).toHaveBeenCalledWith("inner");
expect(fromExecutor).toHaveBeenCalledWith("from a thenable");
expect(onRejected).toHaveBeenCalledTimes(1);
expect(onRejected.calls[0].args[0]).toBe(error);
const existing = new MyPromise(() => {});
expect(MyPromise.resolve(existing)).toBe(existing);
const onFulfilled = spy();
const onRejected = spy();
MyPromise.resolve(5).then(onFulfilled);
MyPromise.reject("no").then(undefined, onRejected);
await flush();
expect(onFulfilled).toHaveBeenCalledWith(5);
expect(onRejected).toHaveBeenCalledWith("no");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with A promise from scratch. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.