Tags: web-dev concept

Shims and Polyfills

Date: 2026-08-17


A shim is code that sits in front of an existing interface to change or normalise its behaviour. A polyfill is a shim that supplies a standard API where it’s missing. Every polyfill is a shim; most shims are not polyfills — and the difference decides whether you can ever delete it.


The family

is the thing you need MISSING, or PRESENT-BUT-WRONG?

  MISSING, and it's a standard        → POLYFILL
                                        Object.hasOwn on an old engine

  MISSING, and you'd rather not
    touch the global                  → PONYFILL
                                        import it explicitly, patch nothing

  PRESENT, but doesn't behave
    how you need                      → SHIM
                                        history.pushState fires no event

  PRESENT, and you're changing it
    with no discipline                → MONKEY-PATCH
                                        a shim you'd be embarrassed to explain

  PRESENT, but the wrong SHAPE
    for your code                     → ADAPTER / WRAPPER
                                        translating one interface into another

The term “polyfill” was coined by Remy Sharp around 2010, specifically for the “missing standard API” case — before that everything here was called a shim. That history is why the two words get used interchangeably, and why being precise about them is worth the effort.

The test that separates them

Ask what happens when the platform catches up.

POLYFILL                             SHIM

the standard arrives                 the standard never arrives —
  → the polyfill becomes dead code     the behaviour you wanted was
  → DELETE IT                          never specified
                                     → the shim is PERMANENT until you
temporary by design                    stop needing the behaviour
has an expiry condition
                                     no expiry condition

A polyfill with no removal plan is technical debt with a timer on it. A shim is architecture — it’s part of how your system works, and it should be treated and documented as such rather than as a temporary patch — Technical Debt.

Worked: a shim

The canonical browser example, and the reason it’s a shim rather than a polyfill — history.pushState exists and works perfectly. It simply doesn’t fire an event, and it never will, because the specification doesn’t say it should.

// wrap the existing methods so a URL change is observable
['pushState', 'replaceState'].forEach((name) => {
  const original = history[name];
  history[name] = function (...args) {
    const result = original.apply(this, args);      // preserve behaviour
    window.dispatchEvent(new Event('locationchange'));   // add ours
    return result;
  };
});

Nothing was missing. The behaviour was inadequate for a purpose the platform didn’t anticipate — The History API.

Worked: a polyfill

// a genuinely absent standard method, supplied conditionally
if (!Object.hasOwn) {
  Object.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
}

The if is the whole discipline. An unconditional assignment overwrites a native implementation that’s likely faster and definitely more correct — and it will keep overwriting it for years after the polyfill stopped being needed.

The risk of patching globals

Both shims and polyfills usually modify something you don’t own, and that has consequences worth pricing:

  • Two libraries patching the same thing fight, and the loser is whichever loaded first. The failure is order-dependent and therefore intermittent
  • The patch is invisible at the call site. Someone reading history.pushState(...) has no way to know it’s been wrapped, which makes debugging genuinely hard — Abstraction and Leaky Abstractions
  • A slow or throwing patch poisons every caller, including third-party code that never asked for it
  • Native implementations usually win on performance, so a permanent polyfill is a permanent tax

Ponyfills avoid all of this by exporting a function you import rather than patching a global. Where you control the call sites, prefer one — the only cost is that it doesn’t help code you don’t control.

Practical rules

  • Always feature-detect, never version-detect. if (!Object.hasOwn), never a user agent check — Browser Compatibility
  • Record why each polyfill exists and what removes it — a browserslist target, a dependency upgrade. Undocumented polyfills are never removed because nobody knows what they were for
  • Load polyfills conditionally, so modern engines don’t pay for them. The build can do this from your target — Transpilation, Bundle Analysis
  • Keep a shim in one named module, not scattered. A wrapped global defined in three files is a debugging nightmare
  • Wrap, don’t replace. Call the original and add to it, as above, so you don’t silently drop behaviour someone depends on
  • Review polyfills annually against your actual traffic. They accumulate and are rarely audited

Where it interacts