What is a polyfill?
Polyfill is JavaScript code that implements a standard web API missing in an older browser, making modern features work as if they were natively supported.
Theory
TL;DR
- A polyfill is like a power adapter for your phone charger: it converts the interface so your existing code connects without changes.
- Polyfill adds a missing API entirely. A shim patches a broken native one. A transpiler like Babel rewrites syntax.
- Use when targeting browsers without ES6+ support and your build tool doesn't cover it automatically.
- Always guard with
if (!API)before defining, or you'll overwrite a faster native method.
Quick example
// IE11 has no Array.prototype.includes
const arr = [1, 2, 3];
arr.includes(2); // TypeError in IE11
// Add the polyfill at the top of your entry file
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
return this.indexOf(searchElement, fromIndex) >= 0;
};
}
arr.includes(2); // true - works in IE11 nowThe guard if (!Array.prototype.includes) is the entire trick. If the browser already has the method natively, it skips the block. If not, the method is added to the prototype chain and every array in that runtime picks it up automatically.
Polyfill vs shim vs transpiler
These three come up together in interviews. A polyfill implements a missing API completely, matching the spec's method signature, return values, and edge cases. A shim fixes an existing but broken or partial implementation. A transpiler like Babel rewrites syntax at build time (async/await to promise chains, for example) but adds no runtime APIs.
The distinction matters here: transpiling arr.includes() to arr.indexOf() >= 0 handles the syntax. But if includes is genuinely absent from the prototype at runtime, you still need a polyfill.
How it works internally
When you assign to Array.prototype.includes, the JS engine registers it as a property on the Array prototype object. Any call to arr.includes() walks the prototype chain, finds your function, and invokes it. No recompilation. The polyfill patches the global object live, so all code in that runtime shares one definition.
That also explains the order rule: the polyfill must be registered before any call to the method.
When to use
- You need to support browsers missing a specific API (check caniuse.com first).
- No build tool, so Babel isn't in the picture.
- Node.js below a version that includes the feature natively, for example
Array.prototype.flatbelow v11. - Already using
@babel/preset-envwithuseBuiltIns: 'usage'and core-js: it handles this automatically, no manual polyfills needed.
For most new projects, polyfill.io is worth knowing about. It's a CDN that reads the User-Agent header and injects only the polyfills that specific browser actually needs.
Common mistakes
Polyfilling after first use
arr.includes(2); // TypeError - method doesn't exist yet
if (!Array.prototype.includes) { /* polyfill */ } // too lateThe polyfill must run before any code that calls the method. Put it in the entry file or load it via a <script> tag before the app bundle.
Overwriting the native method
// No guard - always replaces, even in Chrome
Array.prototype.includes = function() { return true; }; // always true!This breaks spec behavior (ignores fromIndex, breaks NaN handling) and removes the JIT-optimized native version. Always wrap with if (!Array.prototype.includes).
Transpile without polyfill
Babel can rewrite syntax but IE11 still has no fetch at runtime. You need both: @babel/preset-env for syntax and a polyfill like whatwg-fetch for the API. Using useBuiltIns: 'usage' with core-js handles both in one step.
Writing your own Promise polyfill
A minimal Promise polyfill using setTimeout stacks up calls and crashes the stack, because it doesn't handle microtasks the way the spec requires. Use tested libraries like core-js instead of writing your own.
Real-world usage
- core-js: powers most Babel setups. React apps targeting IE11 pull it via
@babel/preset-env. - whatwg-fetch: used in Create React App before 2018, still common in legacy enterprise codebases.
- polyfill.io: CDN that delivers only the polyfills each browser needs based on User-Agent.
- React Native: polyfills
requestAnimationFramefor Android below version 5.
Follow-up questions
Q: What is the difference between a polyfill and Babel?
A: Babel rewrites syntax at build time. A polyfill adds missing APIs at runtime. You often need both at the same time.
Q: Why might a polyfill make your app slower after load?
A: Polyfills assign plain functions to prototypes. The JS engine can't apply the same JIT optimizations it uses for native methods. The difference is small for most cases but measurable on hot code paths.
Q: How do you include polyfills in a modern project?
A: Add core-js/stable to your entry file and set useBuiltIns: 'usage' in @babel/preset-env. It tree-shakes and injects only the polyfills your target browsers actually need.
Q: In a micro-frontend setup, what breaks if two shells load different Promise polyfills?
A: They conflict on the global object. One polyfill overwrites the other's Promise, causing .then() chains to behave incorrectly. Fix it with a shared runtime or a single polyfill entry point loaded before any micro-frontend code runs.
Examples
Basic: Array.prototype.includes polyfill
// Without polyfill - throws in IE11
const fruits = ['apple', 'banana', 'orange'];
fruits.includes('banana'); // TypeError: fruits.includes is not a function
// Add polyfill before first use
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
return this.indexOf(searchElement, fromIndex) >= 0;
};
}
// Works in any browser now
fruits.includes('banana'); // true
fruits.includes('grape'); // falseThe check if (!Array.prototype.includes) means this code runs only in browsers that actually need it. In Chrome or Firefox, includes is already native and the block is skipped entirely.
Intermediate: fetch polyfill in a React component
// fetch is missing in IE11 and early Edge
// This pattern mirrors how whatwg-fetch works
if (!window.fetch) {
window.fetch = function(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve({ json: () => JSON.parse(xhr.response) });
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
};
}
// React component - works in IE11 with the polyfill above
function UserList() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(r => r.json())
.then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Output: renders user list in IE11 without errorsIn production you'd use the full whatwg-fetch package rather than this simplified version. The pattern is the same: check for the global, add it if missing, leave native untouched. I've seen this exact setup in enterprise React codebases that had to support IE11 well into 2022.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.