Go to main content
May 14, 2024
Cover image

Every year new features are added to the ECMAScript specification. And I’m sure you’re waiting for these new features so you can use them as soon as possible.

But is it safe to use them right away? Will it work for all users knowing that they probably don’t have the latest browser version?

Unfortunately, it’s basically not safe, because browsers implement these specifications in new versions.

We will see that thanks to polyfill, these new features will work with older browsers as well.

But before we go deep into polyfills, it’s important to know how new features appear in ES specifications.


There is a very strict process for adding new features to ECMAScript, that is managed by a committee called TC39 that is responsible for evolving the specifications.

This committee is really important because, unlike any other language, EcmaScript has no breaking changes. So we need the best API directly, and to be sure that every corner case is handled.

If you have an idea for a new feature to add to the language, you need to write specifications. It needs an exhaustive list of all cases.

Then you have to go through several stages, I will not describe them very much but you can find them in the TC39 process :

  • Stage 0: New proposal, not reviewed by the committee.
  • Stage 1: Proposal under consideration by the committee.
  • Stage 2: The committee has selected a solution, but the design is a draft.
  • Stage 2.7: Validate the design by writing test cases.
  • Stage 3: Implementation begins, there may be some minor changes to the specification due to the implementation.
  • Stage 4: The feature is ready to be added to the specifications. You’ve done it!

During stage 2, polyfills are drafted to help design the API. This polyfill will be useful once the feature is officially included in the specification, because it will only be implementd in the latest browser versions.

But what the heck is a polyfill?


A polyfill is a piece of code (usually JavaScript on the Web) used to provide modern functionality on older browsers that do not natively support it.

Source mdn: https://developer.mozilla.org/en-US/docs/Glossary/Polyfill

I want to use this new feature that has just been added, for example Promise.withResolvers but I don’t want only 66% of my users to be able to access my site. So I will use a polyfill that implements Promise.withResolvers for older browsers.

// If `Promise.withResolvers` does not exist
// let's implement it
if (!('withResolvers' in Promise)) {
  Promise.withResolvers = () => {
    let resolve, reject;
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });

    return { promise, resolve, reject };
  };
}

or with Array.prototype.flatMap:

if (!('flatMap' in Array.prototype)) {
  Array.prototype.flatMap = (cb) => {
    return this.reduce((acc, current) => {
      const result = cb(current);

      if (Array.isArray(result)) {
        return acc.concat(result);
      }

      return acc.concat([result]);
    }, []);
  };
}

Without polyfill

Available polyfills

Array.prototype forEachmapflatMap

Browser without polyfills

Array.prototype forEachmapflatMap (missing) [].flatMap(v => v)

With polyfill

Available polyfills

Array.prototype forEachmapflatMap

Browser with polyfills

Array.prototype forEachmapflatMap (polyfilled) [].flatMap(v => v)

Pretty cool, right?


There are 2 ways to polyfill missing features:

  • with global environment pollution:

In this case, we add a custom implementation of flatMap to Array.prototype only if it doesn’t already exist:

if (!('flatMap' in Array.prototype)) {
  Object.defineProperty(Array.prototype, 'flatMap', {
    value: (cb) => {
      return this.reduce((acc, current) => {
        const result = cb(current);

        if (Array.isArray(result)) {
          return acc.concat(result);
        }

        return acc.concat([result]);
      }, []);
    },
    enumerable: false,
    configurable: true,
    writable: true,
  });
}
  • without global environment pollution:

In this case, we create a _flatMap function and do not touch Array.prototype.

const _flatMap = () => 
  // Let's use the real implementation if it exists
  if ('flatMap' in Array.prototype) {
    return Array.prototype.flatMap;
  }

  return (cb) => {
    return this.reduce((acc, current) => {
      const result = cb(current);

      if (Array.isArray(result)) {
        return acc.concat(result);
      }

      return acc.concat([result]);
    }, []);
  };
};

I’m sure that you’re wondering:

Polyfills are really useful, but do I have to implement them myself?

Fortunately, no!

There are several libraries that provide these polyfills with different strategies such as polyfill.io and core-js which are the 2 main libraries with completely different strategies:

  • core-js adds polyfills at compile time.
  • polyfill.io fetches needed polyfills at runtime.

In this article we will focus on core-js.

You can see in the repository that there are several packages, in particular:

  • core-js: contains polyfills to do polyfication with global environment pollution.
  • core-js-pure: contains polyfills to do polyfication without global environment pollution.

Now that we know that core-js provides polyfills to support old browsers, the next step is to know which polyfills I want to add.

The question is how do we choose which polyfill to include?

We could include the ones that are available, but many would be unnecessary. Because very old versions of browsers that do not have basic features (today), are not used anymore.

So it would be convenient to patch only browsers that are used.

We need 2 information:

  • What is the oldest browser version that supports this feature?
  • Which browser versions that are used (and their percentage of usage)?

As new features are added to CSS, HTML and Javascript they are implemented in new versions of browsers. So it’s important to know which features are available in which versions so you can make your site available on as many devices as possible.

Can I Use is a site that is always up to date on browser compatibility.

Screenshot the caniuse site

Feature compatibility is obtained from the @mdn/browser-compat-data . Unfortunately, this only displays in which browser versions the features are implemented in. But, it doesn’t take into account, if there are some bugs in the implementation in any version.

By default, babel uses another library called compat-table , which has the same problem.

Fortunately for us, there is an alternative that has been implemented by the creator of core-js Denis Pushkarev, called core-js-compat .


We could track browser usage ourselves thanks to sites like Browser & Platform MarketShare . But that would be very annoying.

Fortunately, caniuse-lite does it for us.

And we can, thanks to Browserslist we ask for versions that match a query.

For example, we can ask for browser versions used by more than 0.5% of users.

import browserslist from 'browserslist';

const versions = browserslist('> .5%');
/*
Returns:
[
  'and_chr 122',       'and_uc 15.5',
  'android 122',       'chrome 122',
  'chrome 121',        'chrome 120',
  'chrome 119',        'chrome 109',
  'edge 122',          'edge 121',
  'firefox 122',       'ios_saf 17.3',
  'ios_saf 17.2',      'ios_saf 17.1',
  'ios_saf 16.6-16.7', 'ios_saf 16.1',
  'ios_saf 15.6-15.8', 'op_mob 73',
  'opera 106',         'safari 17.2',
  'samsung 23'
]
*/

So, thanks to Browserslist we know which browsers we want to patch.


With the list of browser versions we can get the features we need to polyfill.

The data format of core-js-compat is the following object:

const data = {
  'es.symbol': {
    chrome: '49',
    edge: '15',
    firefox: '51',
    hermes: '0.1',
    safari: '10.0',
  },
}

The “number” value is the oldest version that supports the feature.

So it’s pretty easy to extract features. But even easier, the library contains browserslist directly:

import compat from 'core-js-compat';

const {
  list,
} = compat({
  // Browserslist target
  targets: '> .5%',
});
/*
[
  'es.array.push',
  'es.array.to-reversed',
  'es.array.to-sorted',
  'es.array.to-spliced',
  ...
]
*/

And now we can include polyfills by doing:

// Of course this is pseudo code
// In reality at compile time you add it in your file
list.map(module => `import core-js/modules/${module}`);

Thanks to babel-preset-env , we can do what we previously did with just a little configuration.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3.22",
        "targets": "> .5%"
      }
    ]
  ]
}

To be sure, to use new ES features and get polyfills, you must always update your core-js version and be sure to update the version of corejs in the @babel/preset-env configuration.

Tips: If you are using a javascript file for babel, you can configure @babel/preset-env this way and you will not need to update this configuration anymore:

const corejsVersion = require('core-js/package.json').version;


module.exports = {
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": corejsVersion,
        "targets": "> .5%"
      }
    ]
  ]
}

To make sure you’re getting only usefull polyfills, it’s important to keep your caniuse-lite version up to date.

You can do this thanks to the following command:

npx browserslist@latest --update-db

You can see the whole process that happens during polyfication:

Step 1
Code with new ES features
// Tells to core-js what type of polyfills to add
// Here we want to only polyfill stable features
import 'core-js/stable';

Promise.withResolvers();

See you in a future article to see the different configuration with babel.


📝 core-js-compat is better than mdn

🔥 Nice core-js documentation


You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.