Polyfills - Bridging Gaps in JavaScript
Imagine building a beautiful modern web application, only to discover it breaks in older browsers because they don't support the latest JavaScript features. This is where polyfills come to the rescue. Let's explore how these clever pieces of code help bridge the gap between modern JavaScript and older environments.
What is a Polyfill and Why is it Important?
Understanding Polyfills
A polyfill is a piece of code (usually JavaScript) that provides modern functionality on older browsers that don't natively support it. The term was coined by Remy Sharp and comes from the idea of "filling in the gaps" in browser support—like polyfilla (a UK brand of wall filler) fills cracks in walls.
// Modern code you want to write
const numbers = [1, 2, 3, 4, 5];
const hasThree = numbers.includes(3); // ES2016 feature
// But Internet Explorer 11 doesn't support Array.includes()!
// A polyfill makes this work in older browsers
Why Are Polyfills Important?
1. Browser Compatibility Not all users update their browsers immediately. Some organizations are stuck with older versions due to legacy systems or policies.
2. Progressive Enhancement You can use modern features while ensuring your site works for everyone.
3. Backward Compatibility Write code using the latest standards without worrying about older environments.
4. Transition Period During the adoption phase of new JavaScript features, polyfills keep your application accessible.
Real-World Scenario
Let's say you're building a search feature and want to use Array.includes():
function searchUsers(users, searchTerm) {
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// Works perfectly in modern browsers
// Crashes in IE11: "Object doesn't support property or method 'includes'"
Without a polyfill, users on older browsers would see a broken search feature. With a polyfill, everyone gets a working application.
How JavaScript Engines Work and Why Polyfills Are Needed
JavaScript Engine Evolution

JavaScript engines (like V8 in Chrome, SpiderMonkey in Firefox, or Chakra in old Edge) are the programs that execute JavaScript code. Each engine implements JavaScript features based on ECMAScript specifications.
The Implementation Timeline

Feature Detection Flow
Here's how polyfills work with feature detection:

Writing Your Own Polyfills - Step by Step
The Polyfill Pattern
Every polyfill follows this basic pattern:
// 1. Check if the feature exists
if (!SomeObject.someMethod) {
// 2. If not, define it
SomeObject.someMethod = function() {
// 3. Implement the functionality
};
}
Example 1: Array.includes() Polyfill
Let's write a polyfill for Array.includes(), introduced in ES2016:
Step 1: Understand the specification
// How Array.includes() should work:
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, 3].includes(2, 2); // false (start from index 2)
[1, 2, NaN].includes(NaN); // true (special case)
Step 2: Feature detection
if (!Array.prototype.includes) {
// Polyfill needed!
}
Step 3: Implement the polyfill
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
'use strict';
// Handle null/undefined
if (this == null) {
throw new TypeError('Array.prototype.includes called on null or undefined');
}
const array = Object(this);
const len = parseInt(array.length) || 0;
// No elements to search
if (len === 0) {
return false;
}
// Calculate starting index
let startIndex = parseInt(fromIndex) || 0;
let index;
if (startIndex >= 0) {
index = startIndex;
} else {
// Negative index counts from end
index = len + startIndex;
if (index < 0) {
index = 0;
}
}
// Search for the element
while (index < len) {
const currentElement = array[index];
// Special case: NaN === NaN should be true
if (searchElement === currentElement ||
(searchElement !== searchElement && currentElement !== currentElement)) {
return true;
}
index++;
}
return false;
};
}
// Test it!
console.log([1, 2, 3].includes(2)); // true
console.log([1, 2, 3].includes(4)); // false
console.log([1, 2, NaN].includes(NaN)); // true
console.log([1, 2, 3].includes(2, 2)); // false
Example 2: Object.assign() Polyfill
Object.assign() was introduced in ES2015 and is crucial for object manipulation:
if (typeof Object.assign !== 'function') {
Object.assign = function(target) {
'use strict';
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
// Loop through all source objects
for (let index = 1; index < arguments.length; index++) {
const nextSource = arguments[index];
if (nextSource != null) {
// Copy all enumerable own properties
for (const nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
// Usage
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = Object.assign({}, obj1, obj2);
console.log(merged); // { a: 1, b: 3, c: 4 }
Example 3: String.startsWith() Polyfill
A simple but useful string method from ES2015:
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(searchString, position) {
position = position || 0;
return this.substr(position, searchString.length) === searchString;
};
}
// Usage
console.log('Hello World'.startsWith('Hello')); // true
console.log('Hello World'.startsWith('World', 6)); // true
console.log('Hello World'.startsWith('hello')); // false
Example 4: Promise.finally() Polyfill
For handling promise cleanup, introduced in ES2018:
if (typeof Promise.prototype.finally !== 'function') {
Promise.prototype.finally = function(callback) {
const constructor = this.constructor;
return this.then(
// Success handler
value => constructor.resolve(callback()).then(() => value),
// Error handler
reason => constructor.resolve(callback()).then(() => {
throw reason;
})
);
};
}
// Usage
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log('Cleanup: Hide loading spinner'));
Common Polyfills Every Developer Should Know
1. Array Methods Polyfills
Array.find()
if (!Array.prototype.find) {
Array.prototype.find = function(predicate) {
if (this == null) {
throw new TypeError('Array.prototype.find called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
const list = Object(this);
const length = list.length >>> 0;
const thisArg = arguments[1];
for (let i = 0; i < length; i++) {
const value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return value;
}
}
return undefined;
};
}
// Usage
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: 'Bob' }
Array.from()
if (!Array.from) {
Array.from = function(arrayLike) {
const items = Object(arrayLike);
if (arrayLike == null) {
throw new TypeError('Array.from requires an array-like object');
}
const len = items.length >>> 0;
const result = new Array(len);
for (let i = 0; i < len; i++) {
result[i] = items[i];
}
return result;
};
}
// Usage
const nodeList = document.querySelectorAll('div');
const array = Array.from(nodeList);
console.log(Array.isArray(array)); // true
2. Object Methods Polyfills
Object.keys()
if (!Object.keys) {
Object.keys = function(obj) {
if (obj !== Object(obj)) {
throw new TypeError('Object.keys called on non-object');
}
const keys = [];
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
keys.push(key);
}
}
return keys;
};
}
// Usage
const person = { name: 'Alice', age: 30, city: 'NYC' };
console.log(Object.keys(person)); // ['name', 'age', 'city']
Object.values()
if (!Object.values) {
Object.values = function(obj) {
if (obj !== Object(obj)) {
throw new TypeError('Object.values called on non-object');
}
const values = [];
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
values.push(obj[key]);
}
}
return values;
};
}
// Usage
const scores = { math: 95, science: 87, english: 92 };
console.log(Object.values(scores)); // [95, 87, 92]
3. String Methods Polyfills
String.repeat()
if (!String.prototype.repeat) {
String.prototype.repeat = function(count) {
if (this == null) {
throw new TypeError('String.prototype.repeat called on null or undefined');
}
const str = String(this);
count = Math.floor(count);
if (count < 0 || count === Infinity) {
throw new RangeError('Invalid count value');
}
if (count === 0) {
return '';
}
let result = '';
for (let i = 0; i < count; i++) {
result += str;
}
return result;
};
}
// Usage
console.log('*'.repeat(5)); // "*****"
console.log('Hello'.repeat(3)); // "HelloHelloHello"
4. Function Methods Polyfills
Function.bind()
if (!Function.prototype.bind) {
Function.prototype.bind = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind called on non-function');
}
const fn = this;
const args = Array.prototype.slice.call(arguments, 1);
return function() {
const finalArgs = args.concat(Array.prototype.slice.call(arguments));
return fn.apply(context, finalArgs);
};
};
}
// Usage
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined (or error in strict mode)
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42
Best Practices for Using Polyfills
1. Always Feature-Detect First
// GOOD ✓
if (!Array.prototype.includes) {
// Add polyfill
}
// BAD ✗
// Blindly adding polyfills without checking
Array.prototype.includes = function() { /*...*/ };
2. Use Established Polyfill Libraries
For production, consider using well-tested libraries:
// core-js - Most comprehensive
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// Or selectively import what you need
import 'core-js/features/array/includes';
import 'core-js/features/promise/finally';
3. Use Polyfill Services
<!-- Polyfill.io - Serves only needed polyfills based on user agent -->
<script src="https://polyfill.io/v3/polyfill.min.js"></script>
<!-- Or specify features -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.includes,Promise.prototype.finally"></script>
4. Document Your Polyfills
/**
* Polyfill for Array.prototype.includes
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
* @compatibility IE 11, Edge < 14
*/
if (!Array.prototype.includes) {
// Implementation
}
5. Test in Target Browsers
// Use tools like:
// - BrowserStack
// - Sauce Labs
// - LambdaTest
// To verify polyfills work in actual old browsers
Polyfills vs Transpilers
Key Differences
| Aspect | Polyfills | Transpilers |
| What they handle | Runtime features (methods, APIs) | Syntax features (arrow functions, classes) |
| When they run | At runtime in the browser | At build time before deployment |
| Example tools | core-js, polyfill.io | Babel, TypeScript |
| File size impact | Adds JavaScript code | Transforms existing code |
| Use case | Array.includes(), Promise | async/await, ?. optional chaining |
Example: What Needs What?
// NEEDS TRANSPILER (syntax feature)
const greet = (name) => `Hello ${name}`;
class Person { constructor(name) { this.name = name; } }
const value = obj?.property;
// NEEDS POLYFILL (runtime feature)
[1, 2, 3].includes(2);
Promise.resolve(42);
Object.assign({}, obj1, obj2);
// NEEDS BOTH
async function fetchData() { // Transpiler for async/await
const response = await fetch(url); // Polyfill for fetch
return response.json();
}
Building a Smart Polyfill Loader
Here's a pattern for conditionally loading polyfills:
// polyfill-loader.js
const requiredFeatures = {
'Promise': typeof Promise !== 'undefined',
'Array.includes': Array.prototype.includes,
'Object.assign': typeof Object.assign === 'function',
'fetch': typeof fetch === 'function'
};
const missingFeatures = Object.keys(requiredFeatures)
.filter(feature => !requiredFeatures[feature]);
if (missingFeatures.length > 0) {
console.log('Loading polyfills for:', missingFeatures.join(', '));
// Load polyfill bundle
const script = document.createElement('script');
script.src = '/polyfills/bundle.js';
document.head.appendChild(script);
} else {
console.log('All features supported, no polyfills needed!');
}
Real-World Case Study
The Problem
A company needed to support IE11 for enterprise clients while using modern JavaScript:
// Modern code they wanted to write
const users = await fetch('/api/users').then(r => r.json());
const activeUsers = users.filter(u => u.status === 'active');
const hasAdmin = activeUsers.some(u => u.role?.includes('admin'));
The Solution
// 1. Install core-js and regenerator-runtime
// npm install core-js regenerator-runtime
// 2. Import at app entry point
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// 3. Configure Babel to use polyfills
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
targets: {
ie: '11'
}
}]
]
};
// 4. Add fetch polyfill separately
import 'whatwg-fetch';
// Now the code works in IE11!
The Results
✅ Code works in IE11 and all modern browsers
✅ Only ~50KB added to bundle (gzipped)
✅ Modern browsers use native features (faster)
✅ Maintainable codebase using latest JavaScript
Conclusion
Polyfills are essential tools for modern web development, allowing you to use the latest JavaScript features while maintaining compatibility with older browsers. By understanding how to write and use polyfills effectively, you can:
Write modern code without worrying about browser support
Maintain backward compatibility for all users
Understand JavaScript better by implementing features yourself
Make informed decisions about when to use polyfills vs transpilers
Key Takeaways:
Always feature-detect before applying polyfills
Use established libraries like core-js for production
Understand the difference between polyfills and transpilers
Test in real browsers to verify compatibility
Document your support targets and polyfill choices
Keep bundles small by only including needed polyfills
Remember: The goal is to provide a great user experience for everyone, regardless of their browser. Polyfills help you achieve that goal while embracing modern JavaScript features.
Happy coding! 🚀
Further Reading: