skip to content

In a test suite using Mock Service Worker (MSW), what happens by default to a request that matches no handler, and why do teams start it with onUnhandledRequest: 'error'?

level: seniorimportance: should knowfreq 40%

answer

  1. the default is permissive, not strict
  2. a warning, then the real request
  3. hidden dependency on a live host
  4. failure lands far from the cause
  5. declare passthrough, do not relax

basics

~20 s

By default MSW warns and lets the request proceed to the real network, so an unmocked call fails late and obscurely or quietly hits a live API. Setting onUnhandledRequest to 'error' fails the test at the exact mismatch instead.

solid answer

~50 s

MSW's default policy is `'warn'`: it prints a warning and performs the request as it is. In a test process that means the call leaves for the real network — so a suite can quietly depend on a staging API, pass on a developer's laptop and fail in CI where that host is unreachable, and report the failure as a rendering assertion many lines away from the cause. Passing `onUnhandledRequest: 'error'` to `server.listen()` inverts that: any request with no matching handler fails immediately, pointing at the URL that was not covered. The usual causes it surfaces are a typo in a handler path, a relative path where the app calls an absolute origin, a wrong method, and a new call added to a component that nobody mocked. The price is noise from requests you never intended to mock — assets, fonts, analytics — which you handle by adding explicit passthrough handlers for them rather than by turning the policy back off.

code

javascript · 10 lines
javascript
import { http, passthrough } from 'msw'
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(
  http.get('/assets/*', () => passthrough()),
  ...handlers,
)

server.listen({ onUnhandledRequest: 'error' })

go deeper

for a junior

Know that MSW does not block unmatched requests by default — it warns and lets them through — and that test setups usually pass onUnhandledRequest: 'error' to server.listen().

for a middle

Explain the mechanics of each policy value and what an escaped request actually does in a jsdom process, including why the resulting failure surfaces as an unrelated timeout.

for a senior

Diagnose the CI-only flake this default produces, argue for strict mode as a diagnostic tool, and handle intentional traffic with explicit passthrough handlers rather than by relaxing the policy.

for a principal

Set the standard that the mock layer is a complete, enforced boundary for the whole test estate, and decide how strictness is applied consistently so no suite can quietly acquire a dependency on a live service.

## The default is permissive on purpose MSW's `onUnhandledRequest` option decides what to do with a request that no handler claimed. The default, `'warn'`, prints a message naming the request and then **performs it as-is** — the request continues to whatever host it was addressed to. That default suits MSW's other use case, browser development mode, where you often mock two endpoints and want the rest of the app to keep working against a real backend. In a test suite that same default is a liability, because a test's job is to be deterministic and self-contained, and a permissive default lets it silently stop being either. ## What goes wrong **The suite acquires a hidden dependency.** A handler path has a typo, the request escapes, and if a real host answers, the test may even pass — against live data, not against a fixture. Now the suite's result depends on a service nobody listed as a dependency of the build. **Failures land far from their cause.** More often nothing answers: the environment has no network, DNS fails, or the request hangs until the test times out. What the report shows is `waitFor` timing out on an element that never appeared, or `undefined is not an object` deep inside a component. The engineer debugs the component; the actual defect is one character in a handler's path. **It flakes by environment.** Passing locally because a VPN reaches staging, failing in CI where it does not, is the classic shape. So is slowness: an unmocked call adds real latency, which is why suites sometimes get mysteriously slower after a feature that added a new network call nobody noticed. **Coverage silently rots.** As the app adds calls, the mock layer stops describing the whole surface. Nothing tells you, because nothing is required to fail. ## Making the mismatch loud ```js server.listen({ onUnhandledRequest: 'error' }) ``` Now an unmatched request throws, MSW names the method and full URL, and the failure lands on the request rather than on a downstream symptom. This is the setting most teams converge on for CI, and its main value is diagnostic: it converts a whole class of confusing failures into one obvious message. The four causes it exposes, in rough order of frequency, are a mistyped path; a relative path such as `/api/users` where the app calls `https://api.example.com/users`; the wrong method on the handler; and a genuinely new request the code added since the fixtures were written — which is exactly the signal you want, because it is a real gap in the mock surface. ## Dealing with the noise The common objection is that strict mode fails on requests you never meant to mock. In jsdom that is usually less dramatic than feared — no images, stylesheets or fonts are fetched — but a framework's own runtime calls, telemetry, or a third-party widget can all show up. The right response is to declare them, not to loosen the policy. Add explicit handlers for the URLs that should reach the network and have their resolver return `passthrough()`: ```js import { http, passthrough } from 'msw' export const server = setupServer( http.get('/assets/*', () => passthrough()), …handlers, ) ``` That keeps strictness for everything else while writing down, in code, exactly which traffic is intentionally unmocked — which is far better documentation than a permissive default. `onUnhandledRequest` also accepts a callback form when you need per-request logic, letting you stay silent for a known pattern and fail for everything else. Reach for that only when a URL pattern cannot express the rule. A related habit: keep the strict policy in the shared setup file rather than per test file, so a new test file cannot silently opt out of it. ## Where 'bypass' belongs The third value, `'bypass'`, performs the request without even warning. It is not a test setting — it is for a browser development worker that intentionally mocks a slice of the API while everything else talks to a live backend. Using it in tests reintroduces every failure mode above and removes the warning that might otherwise have hinted at them. ## The judgment being tested An interviewer asking this is checking whether you treat the mock layer as a boundary that should be *complete and enforced*, or as a convenience that happens to cover the calls you thought of. The senior answer names the default, names the failure modes it produces in CI, chooses strict for tests, and knows the escape hatch is an explicit passthrough for named traffic rather than a blanket relaxation.

  • You switch a suite to strict mode and a dozen tests fail on the same asset URL. What do you do?
    Add one handler for that URL pattern whose resolver returns `passthrough()`, so the traffic is explicitly declared as intentionally unmocked, and keep strict mode for everything else. Reverting to the permissive default would hide the other eleven kinds of gap the switch just exposed. If a URL pattern cannot express the rule, use the callback form of onUnhandledRequest.
  • Strict mode reports an unhandled request to an endpoint you are sure you mocked. Where do you look?
    At the predicate, comparing the reported URL character by character with the handler. Nearly always it is a relative path against an absolute cross-origin call, a method mismatch, a trailing-slash or query difference, or a handler file that was never imported into the setup. MSW prints the method and full URL precisely so this comparison is quick.
  • Is onUnhandledRequest: 'error' the right setting for browser development mode too?
    Usually not. Development mode often mocks a slice of the API deliberately while the rest talks to a real backend, so failing on every unmatched request would break the app. The permissive default, or `'bypass'`, fits there. It is worth keeping the two setups separate precisely because their goals differ — determinism in tests, partial mocking in development.

saying these in an interview costs you the question

  • Assumes unmatched requests are blocked or 404 by default
  • Thinks a test that hits the real API is harmless if it passes
  • Turns off strict mode to silence asset-request noise
  • Uses 'bypass' in CI to keep the log quiet
  • Debugs the component when the real fault is the handler path

context