How Polyfills Work in JavaScript: A Complete Guide from You Don't Know JS
A polyfill is a defensive JavaScript snippet that detects missing native APIs and provides fallback implementations, enabling modern code to execute safely on older browsers or runtimes.
A polyfill (also called a shim) adds missing language features to environments that lack native support. According to the getify/You-Dont-Know-JS repository, polyfills follow a specific detection-and-definition pattern that checks for existing functionality before adding compatible alternatives. This technique allows developers to write modern ECMAScript code while maintaining compatibility with legacy browsers and Node.js versions.
What Is a JavaScript Polyfill?
A polyfill is a piece of code that replicates a newer JavaScript API in older environments where it does not exist natively. Unlike transpilation, which rewrites syntax at build time, polyfills run at runtime to inject missing methods or objects into the global scope or built-in prototypes. As explained in Getting Started (Chapter 1), polyfills enable forward-compatibility by letting developers use modern features like Promise.prototype.finally while still supporting Internet Explorer 11 or older mobile browsers.
How Polyfills Work: The Detection Pattern
Polyfills follow a three-step defensive pattern to avoid overwriting native implementations and causing conflicts.
Feature Detection
The code first checks whether the target feature already exists using a conditional statement. In get-started/ch1.md (lines 557-579), the book demonstrates this with if (!Promise.prototype.finally) to determine if the ES2019 method is missing before defining it.
Safe Definition
If the feature is absent, the polyfill assigns a function or value to the appropriate object. For example, Promise.prototype.finally = function … adds the missing method only when the native implementation is unavailable. This guard clause ensures the polyfill becomes a no-op in modern engines while providing the necessary fallback in legacy environments.
Spec-Compliant Behavior
Production-grade polyfills aim to mimic the official ECMAScript specification as closely as possible. However, as noted in the You Don't Know JS source code, minimal inline examples used for illustration may skip edge cases like proper handling of Symbol species or cancellation semantics. For production use, developers should prefer battle-tested libraries from the es-shims organization over handwritten implementations.
When to Use Polyfills
Different scenarios require polyfills to bridge capability gaps between development targets.
Targeting older browsers – When supporting Internet Explorer 11 or legacy mobile browsers, native APIs like Array.prototype.flat or Object.hasOwn are often missing entirely. Include a polyfill that guards against existing implementations using if (!Feature) … to prevent conflicts.
Running code in multiple runtimes – Node.js versions prior to 12, older V8 engines, and embedded browsers expose different global objects (global, window, self). As shown in scope-closures/ch4.md (lines 998-1009), a universal globalThis polyfill normalizes these references across environments.
Using a new language feature in a library – Library authors cannot control their consumers' runtime versions. When using modern methods like Object.hasOwn (ES2022), ship the polyfill with your library or advise consumers to enable Babel's useBuiltIns option to automatically inject necessary fallbacks.
Prototyping or experimentation – For quick demos or internal tools, write a minimal inline polyfill as illustrated in the book, but replace it with a robust version before deploying to production. This approach avoids adding dependencies for temporary use cases.
Production-Grade Polyfill Examples from You Don't Know JS
The You Don't Know JS repository provides concrete implementations for several modern features.
Polyfilling Promise.prototype.finally (ES2019)
In get-started/ch1.md (lines 557-579), the book provides a spec-compliant fallback for the finally method added in ES2019:
if (!Promise.prototype.finally) {
Promise.prototype.finally = function (fn) {
return this.then(
value => Promise.resolve(fn()).then(() => value),
reason => Promise.resolve(fn()).then(() => { throw reason; })
);
};
}
This implementation ensures that cleanup logic runs regardless of whether the promise resolves or rejects.
Polyfilling Object.hasOwn (ES2022)
The Object.hasOwn static method provides a safer alternative to Object.prototype.hasOwnProperty.call(). As demonstrated in objects-classes/ch1.md (lines 628-637), the polyfill checks for existence before defining:
if (!Object.hasOwn) {
Object.hasOwn = function (obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
}
This pattern avoids issues with objects that might have overridden the hasOwnProperty method on their prototype chain.
Polyfilling globalThis Across Environments
Different JavaScript environments expose the global object through different identifiers. The robust cross-environment polyfill from scope-closures/ch4.md (lines 998-1009) handles this fragmentation:
const theGlobalScopeObject =
typeof globalThis !== "undefined" ? globalThis :
typeof global !== "undefined" ? global :
typeof window !== "undefined" ? window :
typeof self !== "undefined" ? self :
(new Function("return this"))();
This code progressively falls back through globalThis, global, window, and self before using a last-resort Function constructor to access the global scope.
Risks and Best Practices
While polyfills enable modern development patterns, they introduce specific risks that require mitigation.
Performance overhead – Feature detection and additional function definitions execute on every page load or module import. Minimize this impact by placing polyfills behind conditional checks and loading them only when necessary.
Spec divergence – Handwritten polyfills often miss edge cases present in the official specification. The Promise.prototype.finally example in the book handles basic resolution but might not account for all interactions with promise chains or subclassing. Use established polyfill libraries for critical production code.
Namespace pollution – Modifying built-in prototypes like Array.prototype or Object.prototype can conflict with other libraries or future standard additions. Always check if (!Target.prototype.method) before assignment, and consider using shams (non-mutating alternatives) when modifying globals is unacceptable.
Summary
- A polyfill detects missing APIs and provides fallback implementations only when native support is absent.
- The defensive pattern uses
if (!Feature)checks followed by safe assignment to built-in objects or prototypes. - You should polyfill when targeting legacy browsers, supporting multiple runtimes, shipping libraries with modern dependencies, or prototyping new features.
- Production code should prefer battle-tested libraries like es-shims over minimal inline examples.
- Risks include runtime performance costs, specification edge-case mismatches, and potential namespace conflicts with other polyfills.
Frequently Asked Questions
What is the difference between a polyfill and a shim?
A shim is any code that intercepts API calls and provides a layer of compatibility, often without strictly following the specification. A polyfill is a specific type of shim that implements a standardized API exactly as defined by the specification, allowing code written for the native feature to work unchanged. According to the You Don't Know JS source code, the terms are often used interchangeably, but polyfills specifically aim for spec-compliant behavior while shims may provide alternative interfaces.
Can polyfills impact application performance?
Yes, polyfills introduce small but measurable overhead. The detection logic runs at parse time or module load time, and the fallback implementations may execute slower than native C++ implementations in the JavaScript engine. For example, a polyfilled Array.prototype.flat written in JavaScript cannot match the performance of the native V8 optimization. Mitigate this by only loading polyfills for environments that need them and removing them once you drop support for older browsers.
Should I write my own polyfills or use a library?
For production applications, you should use established polyfill libraries such as core-js or the es-shims collection rather than writing your own. The examples in getify/You-Dont-Know-JS are educational illustrations that demonstrate the detection pattern, but they may lack edge-case handling, proper Symbol support, or security considerations like CSP (Content Security Policy) compliance. Use these examples to understand the mechanism, but deploy battle-tested dependencies for production code.
When can I safely remove polyfills from my codebase?
You can remove polyfills once your application no longer runs in environments that lack the native feature. Conduct analytics on your user base to determine when browsers like Internet Explorer 11 or Node.js versions below 14 fall below your support threshold. After removing polyfills, implement feature detection in your test suite to ensure the native implementations behave as expected, as subtle differences between polyfill behavior and native implementations can cause regressions in edge cases.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →