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.
An ordinary function, once called, runs until it returns or throws: it can't stop halfway and carry on later. A coroutine can. It can pause in the middle, hand a value out to the code that's running it, and later be resumed exactly where it stopped, with its local variables as they were and, if the outside code wants, a value handed in. The code outside decides when it resumes, and with what.
Picture a clerk filling in a form with you. They ask a question and wait. When you answer, they write it down and carry on to the next question, right where they left off. The clerk hands questions out, you hand answers in, and nothing happens until you answer.
In JavaScript, generator functions, written function*, are coroutines. Calling one doesn't run its body: it returns a generator object, and you resume the body through that object's methods. Inside the body, yield is where it pauses:
| Code | Written | What it does |
|---|---|---|
gen.next(value) | Outside | Resumes the body until its next yield or return. value becomes the result of the yield it was paused at. The first next() starts the body, so a value passed to it is ignored. |
yield x | Inside | Pauses the body and hands x out: the next() that resumed it returns { value: x, done: false }. |
gen.throw(error) | Outside | Resumes the body by throwing error at the paused yield, as if that line had thrown it. A try...catch around the yield can catch it. |
return x | Inside | Ends the body: the call that resumed it returns { value: x, done: true }. |
function* signUp() {
const name = yield "What's your name?";
const email = yield `Thanks, ${name}. Your email?`;
return `Signed up ${name} <${email}>`;
}
const form = signUp(); // nothing has run yet
form.next(); // { value: "What's your name?", done: false }
form.next("Ada"); // { value: "Thanks, Ada. Your email?", done: false }
form.next("ada@example.com");
// { value: "Signed up Ada <ada@example.com>", done: true }
function* careful() {
try {
const answer = yield "Ready?";
return `Got ${answer}`;
} catch (error) {
return `Caught ${error.message}`;
}
}
const check = careful();
check.next(); // { value: "Ready?", done: false }
check.throw(new Error("timed out")); // { value: "Caught timed out", done: true }
A coroutine never resumes itself: the code holding it, often called a runner or a scheduler, decides when. That's what makes coroutines useful for async code. The coroutine yields something that stands for what it's waiting for, such as a promise, and the runner resumes it with the result once that's ready. So const user = yield fetchUser(7) reads like ordinary step-by-step code, though the wait happens in between. Before JavaScript had async functions, the co library ran generators this way, and Redux-Saga runs generators that yield descriptions of work, called effects.
async and await are a coroutine with the runner built in. await pauses the function as yield does, and the engine resumes it when the awaited promise settles: with its value if it fulfills, or by throwing its reason at the await if it rejects. That's why a try...catch around an await catches a rejection, as one around a yield catches gen.throw(error). Both share one limit: a function pauses only at a yield or await in its own body, and a function it calls can't pause it.
Before async functions, libraries like co let you write async code with generators: the generator yields a promise, and a runner resumes it with the promise's value once it's ready. Write that runner, run(genFn, ...args).
run calls genFn(...args) to get a generator and starts it. Each time the generator yields a value, run waits for it and resumes the generator with the result, so const user = yield fetchUser() gets the user. Promises and thenables are waited for; any other value is passed straight back.
When a yielded promise rejects, run throws the reason into the generator at that yield, so the generator can catch it with try...catch.
run returns a promise. It fulfills with the generator's return value, or rejects with any error the generator throws and doesn't catch, including one thrown before its first yield. run itself never throws.
const wait = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
const total = await run(function* (a) {
const b = yield wait(100, 2); // resumes 100 ms later with 2
const c = yield 3; // not a promise: resumes with 3
return a + b + c;
}, 1);
// total is 6
const message = await run(function* () {
try {
yield Promise.reject(new Error("offline"));
} catch (error) {
return "handled " + error.message;
}
});
// message is "handled offline"
const result = await run(function* () {
const a = yield Promise.resolve(2);
const b = yield Promise.resolve(3);
return a * b;
});
expect(result).toBe(6);
const clock = useFakeTimers();
const wait = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
const log = [];
const done = run(function* () {
log.push("start");
const a = yield wait(100, "a");
log.push(a);
const b = yield wait(50, "b");
log.push(b);
return log.length;
});
await clock.tickAsync(99);
expect(log).toEqual(["start"]);
await clock.tickAsync(1);
expect(log).toEqual(["start", "a"]);
await clock.tickAsync(50);
expect(log).toEqual(["start", "a", "b"]);
await expect(done).resolves.toBe(3);
const result = await run(function* () {
const x = yield 5;
const y = yield "text";
const z = yield null;
return [x, y, z];
});
expect(result).toEqual([5, "text", null]);
const thenable = {
then(resolve) {
resolve(42);
},
};
await expect(run(function* () {
const value = yield thenable;
return value + 1;
})).resolves.toBe(43);
const error = new Error("offline");
const result = await run(function* () {
try {
yield Promise.reject(error);
return "not caught";
} catch (caught) {
const retried = yield Promise.resolve("retried");
return caught === error ? "caught, then " + retried : "caught something else";
}
});
expect(result).toBe("caught, then retried");
const error = new Error("offline");
await expect(run(function* () {
yield Promise.reject(error);
})).rejects.toBe(error);
await expect(run(function* () {
yield 1;
throw new TypeError("bad input");
})).rejects.toThrow(TypeError);
let promise;
expect(() => {
promise = run(function* () {
throw new RangeError("too soon");
});
}).not.toThrow();
await expect(promise).rejects.toThrow(RangeError);
await expect(run(function* (a, b) {
return a + b;
}, 2, 3)).resolves.toBe(5);
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 Run a generator as a coroutine. 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.