The disadvantages of Javascript polyfills

A polyfill, also known as a shim, is a user-defined implementation of an API that some browsers provide natively, normalising browser differences.

As a proponent of outside-in development, I see the lure of trying to develop websites as if all browsers are the same. The problem is that browsers are not the same. And tackling the “problem” with a polyfill is problematic. Here’s why:

1. They augmenting host objects

Polyfills augment host and native objects. Experts such as Richard Cornford, David Mark, Thomas Lahn and Kangax have told us this is a bad idea. The latter of which published two articles on the matter: What’s wrong with extending the DOM? and Extending native built-ins.

Here’s a choice snippet if you don’t have time to read the articles:

“DOM extension seemed so temptingly useful […]. But what hides behind this seemingly innocuous practice is a huge load of trouble. […] the downsides of this approach far outweigh any benefits.”

2. Feature detection is not enough

As Peter Michaux shows us in Feature Detection: State of the art browser scripting, the mere presence of an API is not always enough to determine its reliablity. This is where feature testing comes in.

Polyfills tend to just detect the presence of an API. They do not iron out the bugs or inconsistencies found in different browsers. Even if they did, they would have to override the original, whereby the override may contain a reference to it. This is dangerous and unnecessary. Instead, we should use facades as we’ll discuss shortly.

3. They intertwine browser and application logic together

As Nicholas Zakas says in Scalable JavaScript Application Architecture, it is important to decouple application and browser logic. He states:

“Application logic should be written one way for all browsers in order to keep the code maintainable. If you’re using native APIs in your application logic, you can’t help but know what browser is being used because you need to account for browser differences. That means your application logic will always need to be updated as new browsers and new browser versions are released. That’s a recipe for disaster”.

4. We rarely need need the full API

We may not need the full API to solve the problem. We may not even be able to implement a polyfill because there’s just no way to do it. This is why context is important.

We should first look to understand the problem precisely. And then solve that problem only. We rarely need all of an API, which is something we’ll discuss shortly. With polyfills it’s all or nothing.

5. They come with caveats

It takes little effort to find problematic polyfills. Take the ES5 Shim documentation. In describing the Object.create polyfill it states:

“For the case of simply “begetting” an object that inherits prototypically from another, this should work fine across legacy engines.”

Emphasis mine. The word should doesn’t instill confidence. We should, of course, build atop of reliable foundations—we are only as good as our lowest level functions. It continues:

“The second argument is passed to Object.defineProperties which will probably fail either silently or with extreme prejudice.”

Emphasis mine. We shouldn’t expect our team to rely on code like this, much less our users. Any code we write around this is just polishing a turd, as the saying goes.

What should we do instead?

A facade or wrapper, is a design pattern that simplifies an interface to something more complex. This allows us to abstract away the differing browser implementations and bugs. And with the added bonus of being able to simplify the method signature.

Inside the facade there is nothing to stop us using bits of an API, and feature testing various implementations and acting accordingly, much like Peter Michaux shows us in his other article Cross browser Widgets.

Cloning an object is pertinent to this article because Object.create solves this problem. If we want to support modern browsers only i.e ones that provide Object.create, then an implementation might be as follows:

var lib = {};
if(Object.create) {
  lib.cloneObject = function(obj) {
    return Object.create(obj);

As this implementation only uses a small part of the entire API, the exposed method signature has just one argument, solving the precise problem and no more. But what about browsers lacking Object.create?

If we want to degrade gracefully, we don’t have to do anything. If we want to support other browsers add a second fork as follows:

// Code credited to David Mark. Thanks.
var lib = {};
if(Object.create) {
  lib.cloneObject = function(obj) {
    return Object.create(obj);
} else {
  lib.cloneObject = (function() {
    var Fn = function() {};
      return function(obj) {
        Fn.prototype = obj;
        return new Fn();

The context of the problem changed. It got a little harder — but the implementation is still lean and the method signature still works well. But, we didn’t need to worry about recreating Object.create in its entirety.

But, what if we did need the full API? We would need two simple changes: change the name of the function to something more appropriate and expand the method signature to allow for property descriptors:

var lib = {};
if(Object.create) {
  lib.createObject = function(obj, props) {
    return Object.create(obj, props);

What about browsers lacking Object.create? Same as before. Either degrade gracefully or add another fork. This is the very essence of Progressive Enhancement.


Having discussed polyfills in detail, it’s clear that, at best, they are hard to implement and they intertwine browser and application logic. This intertwining is complicated and costly.

At their worst, polyfills have caveats and gaps that cause pain for the developer and broken experiences for users.

Instead, we should use facades, which enable us to build reliable, progressively enhanced, and in inclusive experiences. This, of course, results in happy users.

Join my newsletter

If you want to read about designing inclusive, simple and human experiences on the web, you'll probably love my newsletter.