Skip to main content

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

js
// 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 now

The 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.flat below v11.
  • Already using @babel/preset-env with useBuiltIns: '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

js
arr.includes(2); // TypeError - method doesn't exist yet if (!Array.prototype.includes) { /* polyfill */ } // too late

The 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

js
// 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 requestAnimationFrame for 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

js
// 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'); // false

The 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

js
// 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 errors

In 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?