Polyfills and transpilers
JavaScript never sits still. New ideas for the language show up constantly. Each one gets debated by a committee, and the ones that survive land on the official proposal list at https://tc39.es/ecma262/, then eventually roll into the formal ECMAScript specification.
But a feature existing in the spec is not the same as it working on a user’s machine. The teams that build JavaScript engines (V8 in Chrome, SpiderMonkey in Firefox, JavaScriptCore in Safari) each set their own priorities. One engine might ship an experimental proposal early because it looks promising, while parking a finalized feature because it’s tedious to implement or low-demand.
The result: any given engine usually supports only part of the full standard at any moment. Older engines lag further behind.
To track who supports what, the community keeps compatibility tables. A thorough one for the language itself lives at https://compat-table.github.io/compat-table/es6/. It’s dense, and you’ll understand more of it as you work through this course.
So here’s the tension. As a developer, you want the newest features. Cleaner syntax, better built-ins, fewer sharp edges. The more good stuff, the better your code reads and the fewer bugs you write.
At the same time, some visitor out there is running an engine that has never heard of the feature you just used. Their browser will choke on it. How do you write modern code and keep it running on old engines?
Two tools cover the two kinds of gap:
- Transpilers handle new syntax and operators.
- Polyfills handle new functions and methods.
The goal of this chapter is to understand what each one does and where it fits in a real workflow. You won’t set up a build pipeline here; you’ll learn the mental model so the tooling makes sense later.
Transpilers
A transpiler is software that translates source code into other source code. The “source to source” part matters: unlike a traditional compiler that turns code into machine instructions, a transpiler reads your modern JavaScript and writes out equivalent JavaScript using older syntax. Same language, older dialect.
It works by genuinely parsing your code, building an internal model of what each construct means, then re-emitting that meaning with syntax an outdated engine can handle.
Take the nullish coalescing operator, ??. JavaScript didn’t gain it until 2020. An older browser reading quantity = quantity ?? 1 has no rule for what ?? means, so it throws a syntax error before your program even runs.
A transpiler recognizes quantity ?? 1 and rewrites it into a form built from operators that have existed for decades:
// before running the transpiler
quantity = quantity ?? 1;
// after running the transpiler
quantity = (quantity !== undefined && quantity !== null) ? quantity : 1;
Both lines mean the same thing: use quantity unless it’s null or undefined, in which case fall back to 1. The second version just spells it out with a comparison and a ternary, which any ancient engine understands.
quantity !== null)
? quantity : 1
Timing is the key detail: transpiling happens before deployment, not in the visitor’s browser. You run the transpiler on your own machine (or on a build server), and it produces the older-syntax files. Those transpiled files are what you upload. The user’s browser only ever sees code it can already parse.
The best-known transpiler for JavaScript is Babel. You point it at a target range of engines and it rewrites whatever those engines can’t parse.
You won’t run it by hand on every save, though. Build systems like webpack (and modern equivalents such as Vite or esbuild) wire the transpiler into your workflow: every time you change a file, it re-transpiles automatically. Once configured, it fades into the background.
Polyfills
New language versions bring more than syntax. They also add built-in functions and methods — things like Math.sign, Array.prototype.includes, Object.fromEntries, and String.prototype.replaceAll.
Consider Math.sign(n). It reports the sign of a number: Math.sign(3.5) gives 1, and Math.sign(-3.5) gives -1. Handy, but some very old engines never shipped it. On those, Math.sign is simply undefined, and calling it throws.
Here nothing about the syntax is new. Math.sign(3.5) is an ordinary function call, grammar an ancient engine parses fine. The only thing missing is the function itself. So there’s nothing for a transpiler to rewrite; you just need to supply the implementation that’s absent.
A script that adds a missing function or method is called a polyfill — it fills in the hole in the engine’s built-in library. The name comes from “polyfilla,” a brand of wall filler used to patch cracks.
The pattern is: check whether the feature exists, and only define it if it doesn’t.
if (!Math.sign) { // if no such function
// implement it
Math.sign = function(number) {
// comparison operators exist even in ancient JavaScript engines
// they are covered later in the course
return number > 0 ? 1 : number < 0 ? -1 : number;
};
}
Two things make this work. First, JavaScript is highly dynamic: you can assign to Math.sign at runtime, replacing or creating built-in members, because objects like Math are just objects you can extend. Second, the implementation leans only on comparison operators, which have existed since the beginning — a positive number reports 1, a negative number reports -1, and zero (or NaN) is handed straight back unchanged.
Math.sign already existsMath.sign is undefinedThe if (!Math.sign) guard isn’t decorative. On a modern engine you don’t want to clobber the native, optimized implementation with your slower hand-written one, so you skip it entirely when the built-in is present.
Writing every polyfill by hand would be miserable. Instead, curated libraries collect them. A widely used one is core-js: it covers a broad range of standard features and lets you pull in only the pieces your target engines actually lack, so you don’t ship patches for methods everyone already supports.
Summary
The point of this chapter is to give you permission to reach for modern, even bleeding-edge, language features without worrying that older engines will break on them.
The safety net has two halves:
- Use a transpiler when you write new syntax or operators, so old engines can parse your code.
- Use polyfills when you rely on newer built-in functions or methods, so those functions exist at runtime.
Together they let your current-style source run on engines that predate the features you used. Once you’re comfortable with the language, a common setup is a build system like webpack with babel-loader, which transpiles automatically as you work; polyfills come in through a library like core-js.
Two references worth bookmarking for checking support:
- https://compat-table.github.io/compat-table/es6/ — for the JavaScript language itself.
- https://caniuse.com/ — for browser-facing features and APIs.
One practical note: Chrome tends to track new language features early, so if a course demo misbehaves, trying it there is a quick sanity check. Most demos run fine on any current browser.