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.
Write promiseAny(iterable), which works like Promise.any without calling it. It fulfills with the value of the first input to fulfill, and ignores rejections as long as another input might still fulfill.
If every input rejects, it rejects with an AggregateError whose errors array holds the reasons in input order, not the order they rejected in. An empty iterable rejects right away, with an AggregateError whose errors is []. Inputs can be plain values, which count as fulfilled, and iterable can be any iterable.
const fastest = await promiseAny([fetchJson(mirrorA), fetchJson(mirrorB)]);
// the first mirror to answer; one mirror being down doesn't matter
try {
await promiseAny([Promise.reject(new Error("a")), Promise.reject(new Error("b"))]);
} catch (error) {
error instanceof AggregateError; // true
error.errors; // [Error("a"), Error("b")]
}
const clock = useFakeTimers();
const after = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
let result;
promiseAny([after(300, "slow"), after(100, "fast")]).then((value) => {
result = value;
});
await clock.tickAsync(300);
expect(result).toBe("fast");
const clock = useFakeTimers();
const failing = new Promise((_, reject) => setTimeout(reject, 50, new Error("mirror A is down")));
failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
const ok = new Promise((resolve) => setTimeout(resolve, 100, "mirror B"));
let outcome;
promiseAny([failing, ok]).then(
(value) => {
outcome = value;
},
(error) => {
outcome = error;
},
);
await clock.tickAsync(100);
expect(outcome).toBe("mirror B");
const clock = useFakeTimers();
const failAfter = (ms, reason) => {
const promise = new Promise((_, reject) => setTimeout(reject, ms, reason));
promise.catch(() => {}); // mark it handled, so it's never reported as unhandled
return promise;
};
let reason;
promiseAny([failAfter(100, new Error("a")), failAfter(200, new Error("b"))]).catch((e) => {
reason = e;
});
await clock.tickAsync(100);
expect(reason).toBeUndefined();
await clock.tickAsync(100);
expect(reason).toBeInstanceOf(AggregateError);
const clock = useFakeTimers();
const failAfter = (ms, reason) => {
const promise = new Promise((_, reject) => setTimeout(reject, ms, reason));
promise.catch(() => {}); // mark it handled, so it's never reported as unhandled
return promise;
};
const first = new Error("first");
const second = new Error("second");
let reason;
promiseAny([failAfter(300, first), failAfter(100, second)]).catch((e) => {
reason = e;
});
await clock.tickAsync(300);
expect(reason.errors).toHaveLength(2);
expect(reason.errors[0]).toBe(first);
expect(reason.errors[1]).toBe(second);
const failing = Promise.reject(new Error("no"));
failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
let result;
promiseAny([failing, 7]).then((value) => {
result = value;
});
await flush();
expect(result).toBe(7);
let reason;
promiseAny([]).catch((e) => {
reason = e;
});
await flush();
expect(reason).toBeInstanceOf(AggregateError);
expect(reason.errors).toEqual([]);
const clock = useFakeTimers();
const after = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
let result;
promiseAny(new Set([after(200, "b"), after(100, "a")])).then((value) => {
result = value;
});
await clock.tickAsync(200);
expect(result).toBe("a");
const builtIn = Promise.any;
Promise.any = () => {
throw new Error("promiseAny called the built-in Promise.any");
};
let promise;
try {
promise = promiseAny([Promise.resolve("mine")]);
} finally {
Promise.any = builtIn;
}
await expect(promise).resolves.toBe("mine");
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 Promise.any. 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.