What they're testing
Whether you understand your build tooling, or copy config until the errors stop.
The short answer~30 seconds
ESM is statically structured: imports are parsed before any line executes, so a bundler knows exactly what's used and can drop the rest (tree shaking). In CommonJS, require() is an ordinary function call evaluated at runtime and legal inside an if — so it can't be fully analysed statically. The pain of mixing comes from ESM loading asynchronously while require is synchronous, which is why CJS can't simply require an ESM module.
The long answer
The most common practical consequence is ERR_REQUIRE_ESM: a library goes ESM-only, your code is still CJS, and there's no way to require it synchronously because ESM may contain top-level await. The escapes are a dynamic await import(), or moving the project to ESM. Node 22 relaxed this with require(esm) for modules without top-level await, but you still need to know where the boundary sits.
A less-discussed second difference: ESM exports live BINDINGS, CJS exports copied values. If module A reassigns an exported variable, an ESM importer sees the new value while a require caller keeps the old one. It's rare, and when it bites it's very hard to trace, because it looks like a logic bug rather than a module bug.
On tree shaking, the thing worth knowing is that it isn't free: it only works when modules are side-effect-free, and bundlers want "sideEffects": false in package.json before they'll confidently drop code. A library that imports a CSS file at module level counts as having side effects, and its whole dependency subtree stays in the bundle. That's a very common reason for "we use ESM and the bundle is still huge".
What they'll ask next
?Does a dynamic import() break tree shaking?
No — it creates a separate chunk, which is code splitting and usually what you want. What breaks it is an import() whose path is built from a variable, because the bundler must bundle every possible match.
?What does "type": "module" do in package.json?
It decides whether .js files are read as ESM or CJS. To mix, use explicit extensions: .mjs is always ESM and .cjs always CJS, regardless of the setting.
These lose points
- Saying "ESM is just new syntax for require". The loading model is fundamentally different, and that's the entire compatibility problem.