Node now provides globals that were once browser-only, such as `fetch`, `AbortController`, `URL` and `structuredClone`. How should shared JavaScript code decide what it is allowed to call?
answer
- hosts converged on web APIs
- ask what, not where
- workers have no window
- jsdom puts window in Node
- inject the capability instead
basics
~20 sTest for the capability you are about to use, not for the host you think you are on. Checks like typeof fetch === 'function' stay correct as runtimes converge, while inferring a host from window or process goes stale and is wrong in workers, test environments and non-browser, non-Node runtimes.
solid answer
~50 sHost sniffing encodes an assumption that keeps expiring. `typeof fetch === 'undefined'` used to mean "Node", and since Node 18 shipped a global `fetch` it means nothing of the sort; `structuredClone` has been global in Node since 17. Sniffing is also wrong in both directions at once: a browser worker has no `window`, a jsdom-style test environment installs `window` inside a Node process, and other runtimes are neither. So branch on the exact capability — `typeof fetch === 'function'`, `typeof localStorage !== 'undefined'` — because that is the question your next line actually asks. Reserve genuinely host-shaped checks for things no polyfill blurs: is there a document to render into, is there a filesystem to read. And when a capability is missing, decide deliberately what the other side does — degrade, defer, or inject the dependency from the caller.
code
javascript · 15 linesexport function createClient({ fetch: fetchImpl = globalThis.fetch } = {}) {
if (typeof fetchImpl !== 'function') {
throw new TypeError('createClient requires a fetch implementation');
}
return {
async getJson(url) {
const res = await fetchImpl(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
};
}
const fake = async () => ({ ok: true, status: 200, json: async () => ({ hi: true }) });
createClient({ fetch: fake }).getJson('/x').then(console.log);go deeper
Know that checking for the feature you need — for example typeof fetch === 'function' — is more reliable than guessing which runtime you are on, because runtimes keep gaining each other's APIs.
Explain why the old markers stopped working: Node adopted fetch, structuredClone, AbortController and URL. Be able to give a case where a window check is wrong in each direction.
Show the design consequence: pick a deliberate behaviour for the missing branch — degrade, defer, or take the capability as a parameter — and keep detection at the edge rather than scattered through the module.
Own the runtime support contract: which runtimes and versions are promised, whether shared packages may read globals at all, and how the CI matrix proves the promise so capability drift is caught before users find it.
## Why the old sniffing habits stopped working For years the shorthand worked well enough: `typeof window !== 'undefined'` meant browser, `typeof process !== 'undefined'` meant Node, and the presence of `fetch` was a browser fingerprint. Every one of those has since become unreliable. Node has steadily adopted web-standard globals: `URL` and `URLSearchParams`, `TextEncoder`/`TextDecoder`, `AbortController` and `AbortSignal`, `structuredClone` (Node 17), a global `fetch` with `Request`/`Response`/`Headers` (Node 18, stable in 21), `queueMicrotask`, `performance`. Code that treated any of those as a browser marker now mis-detects. The converse holds too: - A browser **worker** context has no `window` — but it is unambiguously a browser, with `fetch`, `URL` and `structuredClone` all present. - A jsdom-style **test environment** installs `window` and `document` inside a Node process, so a `window` check reports "browser" while `process` and the filesystem are right there. - Other **JavaScript runtimes** exist — server runtimes and edge environments that implement web APIs without being a browser and without being Node. Binary host detection has no correct answer for them. The deeper problem is that a host check answers a question you did not ask. Your next line does not need to know what the host is; it needs to know whether a specific function exists. ## Detect the capability ```js const canFetch = typeof fetch === 'function'; const canClone = typeof structuredClone === 'function'; const canStore = typeof localStorage !== 'undefined'; const canObserve = typeof IntersectionObserver === 'function'; ``` Three properties make these better than host guesses. They are **local** — the condition sits next to the call it protects, so a reader sees exactly why the branch exists. They are **stable** — when a runtime gains the feature, the check simply starts returning `true` and the good path runs, with no code change. And they are **honest** — they claim only what they tested. Prefer `typeof X === 'function'` over truthiness for callables: it rejects a non-callable stand-in and does not throw on an undeclared name. ## When a host check is still the right check Some questions genuinely are about the host, because no polyfill makes them ambiguous in practice: - "Is there a document to render into or listen on?" — `typeof document !== 'undefined'`. - "Do I have a filesystem and a process to read configuration from?" — this is where `process` legitimately appears. Even then, prefer the narrowest phrasing. `typeof document !== 'undefined'` says "a DOM exists", which is what your code needs; `isBrowser()` says something broader that you cannot actually verify. And be aware that `typeof process !== 'undefined'` is not proof of Node — browser bundles sometimes carry a small `process` stand-in so that `process.env` references survive, which is enough to fool a naive check. ## Decide what the missing branch does A capability check is only half a design; the other half is the fallback, and it should be a deliberate choice rather than a silent `return`: - **Degrade**: return a sensible default and carry on (`getTheme()` returns `'light'` where there is no storage). - **Defer**: do nothing now and let the browser-side lifecycle run it later — right for effects like measuring, focusing or observing. - **Delegate**: take the capability as a parameter. A module that accepts `{ fetch }` from its caller has no detection code at all, is trivially testable, and works in any runtime the caller can satisfy. Delegation is the senior move: it converts an environment question into an ordinary dependency, and dependencies can be substituted, mocked and reasoned about. ```js export function createClient({ fetch: fetchImpl = globalThis.fetch } = {}) { if (typeof fetchImpl !== 'function') { throw new TypeError('createClient requires a fetch implementation'); } return { get: (url) => fetchImpl(url).then((r) => r.json()) }; } ``` Note the default read through `globalThis`, which is safe in every host, and the loud failure when nothing satisfies the requirement — far better than an obscure `undefined is not a function` three layers deeper. ## Version-sensitivity, stated honestly If your code relies on `fetch` being global rather than injected, you are declaring a Node floor of 18. That belongs in the package's stated engine range and in your CI matrix, not in a comment. Capability detection tells you what is there at runtime; the supported-version statement tells your users what you promise. Both are needed, and conflating them is how a library ends up silently degrading on the very versions it advertises. ## The answer in one breath Ask what you need, not where you are. Capability checks age well, host checks age badly, and the best shared modules avoid the question entirely by accepting the capability from their caller.
- Give a concrete case where `typeof window !== 'undefined'` gives the wrong answer.Two, in opposite directions. Inside a browser worker there is no `window`, yet the code is running in a browser with `fetch` and `structuredClone` available. And a jsdom-style test environment defines `window` inside a Node process, so the check reports "browser" while a filesystem and `process` are present.
- Why is injecting a capability better than detecting it inside a module?It turns an environment question into an ordinary parameter. The module gets simpler — no branches, no `typeof` — and becomes testable with a stub, portable to runtimes you never anticipated, and explicit about its requirements. Detection remains useful only at the outermost layer that actually chooses the implementation.
- If you rely on a global `fetch` rather than injecting one, what else must you do?State the runtime floor. Relying on a global `fetch` means declaring Node 18 or newer in your package's engine range and testing against it, because runtime detection alone would just make the library quietly degrade on the versions you claim to support.
saying these in an interview costs you the question
- Treats absence of fetch as proof of Node
- Assumes typeof process means a real Node runtime
- Equates window with browser in all contexts
- Sniffs navigator.userAgent to pick a code path
- Silently no-ops when a capability is missing, with no fallback decided