Use this JavaScript compatibility checklist to decide whether an ES feature can ship as written, needs syntax transformation, requires a runtime polyfill, or should be replaced for a particular browser and Node target.
Overview
JavaScript compatibility is not one question. A feature may be understood by the parser but still fail at runtime because an API is missing. Conversely, a browser may support an API while your build pipeline still transforms the syntax for older targets. Separating these cases prevents unnecessary polyfills and catches failures that a successful build cannot detect.
Use the matrix below as a planning guide, not as a substitute for checking the exact browser versions and Node releases supported by your application. Browser support changes as engines ship updates, and your real target may include embedded webviews, older enterprise devices, server-side rendering, or test runners with different capabilities.
| Feature group | What compatibility involves | Typical action | Validation question |
|---|---|---|---|
| Classes, arrow functions, template literals, destructuring | JavaScript syntax parsing and execution | Transpile for older syntax targets; no polyfill fixes a parser error | Can every target parse the emitted bundle? |
| Promises, Map, Set, Symbol | Syntax plus built-in runtime objects | Transpile where needed and add a focused polyfill when the runtime lacks the object | Does the target provide the constructor and required methods? |
| Array and Object methods | Individual methods such as newer iteration or object helpers | Check methods separately; polyfill only missing methods | Is the exact method supported, not merely the parent object? |
| ES modules and dynamic import | Module loading, bundler output, and execution context | Choose an output format and fallback strategy appropriate to browsers or Node | Will the target load the module form you emit? |
| Optional chaining and nullish coalescing | Parser support and transformed output | Use Babel or TypeScript when an older parser is in scope | Does the deployed bundle contain syntax the target cannot parse? |
| Intl APIs | API availability and locale/data support | Test the specific constructor or method; consider a targeted polyfill or data package | Does the target support the locale and formatting behavior you need? |
| Fetch, URL, AbortController, Web Crypto | Host-provided Web APIs rather than language syntax | Use a browser or server implementation suited to the execution environment | Is this a browser API, a Node API, or both? |
| ReadableStream and related Streams APIs | Runtime API behavior, not just syntax | Check browser and Node support independently and test backpressure assumptions | Does the implementation support the operations your library uses? |
| Top-level await | Module type, parser support, and bundler output | Confirm an ES module execution path and compatible build configuration | Can the runtime and bundler both handle the generated module graph? |
A useful rule is to classify every proposed feature into one of four buckets: native syntax, transformed syntax, native API, or polyfilled API. Babel and TypeScript primarily address syntax and output; core-js and similar packages address selected language built-ins; browser or Node implementations determine host APIs such as fetch, streams, and storage.
For related workflow checks, pair this process with a repeatable browser-based debugging workflow. A compatibility failure is often easiest to isolate in the actual target runtime rather than from the source code alone.
Checklist by scenario
Starting a browser application
- Write down supported browser families and the oldest version that matters. Include mobile browsers and embedded webviews if they are part of the product.
- Define a Browserslist query in package metadata or a dedicated configuration file. Avoid relying on an undocumented default.
- Set the bundler target to match that audience. A modern output target can leave syntax that an older browser cannot parse.
- Use Babel, the TypeScript compiler, or the framework's supported transform pipeline for syntax conversion.
- Decide whether runtime polyfills are needed. Test APIs such as Promise, URL, fetch, and AbortController separately from syntax.
- Build a production bundle and inspect it. Confirm that the generated code, module format, and polyfill strategy agree with the deployment target.
Shipping a library
- State the minimum runtime your package supports rather than silently choosing one.
- Prefer non-invasive output for libraries. Automatically modifying a consumer's global environment can create conflicts.
- Publish the module formats your consumers need, such as an ESM build and a compatible fallback where appropriate.
- Test the package from a clean consumer project. A library can pass its own tests while failing when imported by another bundler.
Deploying a Node application
- Pin or clearly document the Node range used in development, CI, and production.
- Separate server runtime APIs from browser APIs. A browser polyfill does not automatically make a Node process behave like a browser.
- Set TypeScript's
targetandmodulefor the actual Node deployment model, including CommonJS or ESM. - Test the built server with the same Node family used in production. Do not infer server compatibility from browser tests.
Using a new ES feature in code review
- Identify whether the feature is syntax, a built-in, or a host API.
- Check the project's declared targets and generated output, not just the developer's local browser or Node version.
- Ask whether a transform changes semantics or debugging quality. Some features are better left native when all targets support them.
- Add a focused test for the fallback path if a polyfill or alternate implementation is introduced.
Copy-ready build settings
These examples are starting points. Adjust them to the versions and module conventions your project actually supports.
Browserslist
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"not op_mini all"
]
}
For a controlled application, an explicit product-supported browser list is usually easier to review than a vague phrase such as “modern browsers.” Re-run the build after changing it and inspect the resulting bundle.
Babel with targeted built-ins
module.exports = {
presets: [["@babel/preset-env", {
useBuiltIns: "usage",
corejs: 3
}]]
};
This pattern requires the corresponding core-js dependency and should be verified against the Babel and core-js versions installed by the project. If you do not want automatic global polyfills, use a different strategy and document who supplies the required APIs.
TypeScript
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true
}
}
The target controls much of the emitted syntax, but TypeScript does not provide runtime APIs. If the output calls a missing Promise, method, or Web API, you still need a runtime solution or a supported alternative.
Vite, webpack, and Rollup
// Vite
export default {
build: {
target: "es2020"
}
};
// webpack concept
module.exports = {
module: {
rules: [{
test: /\\.m?js$/,
exclude: /node_modules/,
use: "babel-loader"
}]
}
};
For Rollup, use the Babel or TypeScript plugin that matches your output target, then verify external dependencies separately. Tool defaults vary, so consult the installed version's documentation before copying a production configuration. The frontend build tools comparison can help frame the trade-offs between common bundlers.
What to double-check
- Syntax versus API: optional chaining can be transformed, but transforming code does not create a missing API such as fetch or a newer Array method.
- Browser versus Node: shared JavaScript syntax does not imply shared host APIs. Check each environment independently.
- Module boundaries: dynamic import and top-level await depend on module format, bundler behavior, and runtime loading rules.
- Locale behavior: Intl support can vary in available locale data and formatting details. Test the locales your product promises.
- Dependency output: your application may be transpiled while a dependency still ships unsupported syntax. Check whether the bundler processes that dependency.
- Polyfill scope: decide whether the polyfill is global, injected per entry point, or imported explicitly. Record that decision for future maintainers.
- Production testing: run smoke tests against representative target browsers and the deployed Node runtime. A development server can hide output or loading differences.
Common mistakes
Assuming ES version labels are enough. “ES2020 support” is a useful shorthand, but applications use individual syntax forms and APIs. Check the feature itself and its execution environment.
Using a polyfill to fix a parse error. A runtime polyfill cannot help a browser that rejects the bundle before execution. Transform or remove unsupported syntax first.
Copying a framework default without recording it. Defaults change as frameworks and bundlers evolve. Keep the intended target in version-controlled configuration and CI.
Testing only the newest local runtime. Local success proves little about an older browser, a webview, a server process, or a test runner. Add at least one representative compatibility test.
Polyfilling everything. Broad polyfills increase bundle work and can alter global behavior. Prefer the smallest tested set that meets your support contract.
When to revisit
Recheck this matrix before a release that changes browser support, during seasonal planning or dependency upgrades, and whenever the project moves between Node versions or build tools. Revisit it immediately when introducing a language feature, browser API, module pattern, or dependency with a different output format.
A practical maintenance routine is:
- Review the declared browserslist, Node engines, TypeScript target, and bundler target together.
- Compare new application features against the oldest supported runtime.
- Rebuild and inspect emitted syntax and polyfill imports after tool upgrades.
- Run browser smoke tests, server tests, and any webview or embedded-runtime tests that matter to your users.
- Record exceptions, vendor bugs, or performance concerns beside the affected feature so the next review has context.
Keep a small CSV or spreadsheet version of your team’s matrix with columns for feature, environment, target, evidence, fallback, and review date. That turns compatibility from a one-time guess into a repeatable developer workflow. For surrounding API and debugging work, see the guides to JSON validation, API testing workflows, and web performance testing.