Skip to main content

Command Palette

Search for a command to run...

Polyfills - Bridging Gaps in JavaScript

Updated
10 min read

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

AspectPolyfillsTranspilers
What they handleRuntime features (methods, APIs)Syntax features (arrow functions, classes)
When they runAt runtime in the browserAt build time before deployment
Example toolscore-js, polyfill.ioBabel, TypeScript
File size impactAdds JavaScript codeTransforms existing code
Use caseArray.includes(), Promiseasync/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:

  1. Always feature-detect before applying polyfills

  2. Use established libraries like core-js for production

  3. Understand the difference between polyfills and transpilers

  4. Test in real browsers to verify compatibility

  5. Document your support targets and polyfill choices

  6. 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: