What Is Polyfilling in JavaScript?
Polyfilling in JavaScript means adding a missing API to an environment that does not support it yet. The key word is API. A polyfill can provide Array.prototype.includes or String.prototype.replaceAll. It cannot teach an old JavaScript parser how to understand arrow functions. That is a different job.
The useful mental model: transpilers rewrite syntax before your code runs. Polyfills add missing runtime behavior while your code runs.
The failure a polyfill solves
Imagine you write this:
const names = ["Ada", "Grace", "Katherine"];
console.log(names.includes("Grace"));
In an environment that supports Array.prototype.includes, this logs true. In an older environment without that method, JavaScript tries to call undefined as a function:
TypeError: names.includes is not a function
A polyfill checks whether the method exists and defines it if it does not:
if (!Array.prototype.includes) {
Array.prototype.includes = function (searchValue) {
return this.indexOf(searchValue) !== -1;
};
}
That example is intentionally simplified. Real polyfills need to match the specification closely, including edge cases around NaN, sparse arrays, property descriptors, and strict-mode behavior. This is why production apps usually use maintained polyfill packages instead of handwritten snippets.
Polyfilling versus transpiling
Transpiling changes source code. A tool such as Babel can take newer syntax and emit older syntax:
const double = (n) => n * 2;
can become:
var double = function double(n) {
return n * 2;
};
That works because arrow functions are syntax. If an older parser cannot read =>, the code has to be changed before it reaches the browser.
Polyfilling handles missing objects, functions, methods, and globals:
"hello".replaceAll("l", "x");
If the parser understands this syntax but the method is missing at runtime, a transpiler alone is not enough. Something has to provide String.prototype.replaceAll.
Feature detection
A good polyfill does not blindly overwrite native behavior.
if (typeof Object.assign !== "function") {
Object.assign = function (target, ...sources) {
// Polyfill implementation here.
};
}
The check matters for two reasons:
- Native implementations are usually faster and more accurate.
- Overwriting built-ins can break code that already relies on the native behavior.
Feature detection asks, "Can this environment do the thing?" That is more reliable than guessing from browser names or version strings.
Monkey patching is the risky part
Many polyfills work by adding methods to global constructors or prototypes:
Array.prototype.myMethod = function () {
return "added to every array";
};
That technique is called monkey patching. It is powerful because every array now has the method. It is risky for the same reason.
Application code should almost never invent new methods on built-in prototypes. A standards-compliant polyfill for a real JavaScript feature is one thing. Adding Array.prototype.first because it felt cute on a Tuesday is another.
If you need a helper, write a helper:
function first(array) {
return array[0];
}
No global side effect, no future naming collision, no mystery for the next developer.
What cannot be polyfilled
Polyfills can only add behavior that can be expressed in existing JavaScript. Some features need parser support or engine-level hooks.
New syntax cannot be polyfilled:
const user = { name: "Ada" };
An old parser that does not understand const fails before any polyfill code can run. Use a transpiler for syntax.
Some runtime features cannot be faithfully reproduced either. Proxy depends on engine-level traps. WeakMap depends on garbage-collection behavior that normal JavaScript cannot fully observe or copy. You may find partial shims, but they are not the same thing.
That distinction matters when choosing browser support. Some gaps can be filled. Some gaps require different code or a different minimum runtime.
How modern apps usually handle polyfills
Most production projects do not paste polyfills into random files. They use tooling.
Common approaches include:
- A maintained package such as
core-js. - Babel or another build tool configured to inject needed polyfills.
- A fixed browser-support target that lets the bundler avoid unnecessary fallbacks.
- A small explicit polyfill imported once at the app entry point.
The important part is ownership. Know where polyfills enter your app, which browsers you support, and whether the fallback is global or local.
This is better:
import "core-js/actual/array/to-sorted";
than sprinkling hand-written feature patches through unrelated components.
A realistic example
toSorted() returns a sorted copy of an array, unlike sort(), which mutates the original:
const scores = [3, 1, 2];
const sorted = scores.toSorted();
console.log(sorted); // [1, 2, 3]
console.log(scores); // [3, 1, 2]
In an environment without toSorted, you can get similar behavior with spread plus sort:
const sorted = [...scores].sort((a, b) => a - b);
A real polyfill would make scores.toSorted() available. The fallback above is not a polyfill unless you actually define the missing method on Array.prototype.
Key Takeaways
- A polyfill adds a missing runtime API; it does not rewrite syntax.
- A transpiler rewrites syntax before code runs.
- Good polyfills use feature detection and avoid overwriting native behavior.
- Handwritten polyfills are easy to get subtly wrong; prefer maintained packages for production support.
- Some features cannot be faithfully polyfilled because they require parser support or engine internals.