Write promiseRace(iterable), a polyfill for Promise.race.
Before you start
What's a polyfill?
A polyfill is code that adds a standard feature to an environment that doesn't have it yet, such as Promise.any or Array.prototype.flat in an older browser. It's written in plain JavaScript that the old environment can already run, and it follows the specification, the document that defines how the feature behaves, as closely as it can. Code written for new browsers then works in old ones too. The name comes from Polyfilla, a paste for filling cracks in walls: a polyfill fills the gaps in a browser.
A real polyfill checks for the feature first, as in if (!Promise.any), and defines it only when it's missing, so a browser that has the built-in keeps its own version:
if (!Array.prototype.flat) {
Array.prototype.flat = function flat(depth) {
// the steps the specification lists, written in plain JavaScript
};
}
Every array gets its methods from one shared object, Array.prototype, and every function from Function.prototype. So a method added there appears on every array or function in the program, even ones created before it was added, and inside it this is the array or function it was called on. Careful polyfills add it with Object.defineProperty rather than = as above, so that, like the built-ins, it isn't enumerable and doesn't show up in a for...in loop.
When an interviewer asks you to write a polyfill, they mean: reproduce a built-in's behaviour yourself, without calling it. It shows you know exactly what the built-in does, beyond its everyday use. The details are the point, because code written against the standard relies on them too:
| Built-in | Everyday use | Details a polyfill must match too |
|---|---|---|
Array.prototype.map | [1, 2].map((n) => n * 2) is [2, 4] | thisArg, holes, and the TypeError for a callback that isn't a function, thrown even when the array is empty |
Function.prototype.bind | A function with a fixed this | Preset arguments, and calling the result with new |
Promise.all | Waits for several promises | Plain values among them, an empty input, and an argument that isn't iterable |
Object.create | An object that inherits from the one you pass | null as the prototype, and a TypeError for one that's neither an object nor null |
JSON.stringify | Turns data into text | undefined and functions, toJSON, and a TypeError on cycles |
These tasks give your version its own name, such as Array.prototype.myMap, Object.myCreate, or JSON.myStringify, or make it a stand-alone function, such as promiseAll(iterable) or objectIs(a, b). Replacing the real method would change it for every other piece of code on the page, and leave you no built-in to compare yours with.
In production you rarely write polyfills by hand: libraries such as core-js provide them, and Babel can add the ones your target browsers need. A polyfill can only add missing objects, functions, and methods. New syntax, such as ?. or class, can't be added that way: a compiler has to rewrite it as older syntax, which is called transpiling.
What's a thenable?
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.
Settle the same way as the first input to settle: fulfill with its value or reject with its reason.
Inputs can be promises, thenables, or plain values. With an empty iterable, stay pending forever, like the built-in. A non-iterable argument rejects with a TypeError.
function promiseRace(iterable) {
// your code
}
// const delay = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
// await promiseRace([delay(300, "slow"), delay(100, "fast")]); // "fast"
// await promiseRace([delay(300, "slow"), Promise.reject(new Error("failed"))]); // rejects with Error "failed"
// await promiseRace([delay(300, "slow"), "plain value"]); // "plain value"
Solve it in the editor, with tests
The answer
Write it yourself first. Then compare it with a reference solution and the checks it passes.
Checking your sign-in…