Articles
MSW Interview Questions: Request Handlers, Unhandled Requests, and Test Isolation
Practice MSW interview questions with a browser/Node boundary map, strict unhandled-request policy, and per-test handler isolation.
An MSW test can pass locally while making a real network request in CI. That is the interview failure mode worth understanding. A request handler may be missing, a browser worker may not have started yet, or a previous test may have left a runtime override in place. A strong answer treats mocking as a request lifecycle with explicit setup, matching, error policy, and cleanup, not as a collection of convenient fixture responses.
Official MSW facts here come from Mock Service Worker's documentation. The Checkout API examples, failure ledger, and test plan are editorial exercises, not candidate reports or claims about a particular employer's interview. The exact handler syntax assumes MSW's current http/HttpResponse API; legacy code may use older APIs and should be evaluated against its installed version.
Where does MSW intercept a request?
MSW's quick start separates browser setup with setupWorker from Node setup with setupServer. In the browser, a Service Worker intercepts requests under its scope. In Node tests, MSW intercepts supported network requests without requiring a browser worker. The handlers describe API behavior at the request boundary, which allows application code to keep using normal fetch or client calls.
For a Checkout UI, imagine GET /api/cart and POST /api/checkout. A handler for cart returns three line items. A checkout handler can return success, validation failure, or server error, depending on the test. The app should not need a special “mock fetch” branch. The interview answer should still state where the test runs. A Jest or Vitest Node test and a browser component test may need different setup even when they share the same handler definitions.
MSW does not automatically intercept every request from every possible environment. In a browser test, the worker must be installed, started, and controlling the page before the app issues the request. The browser integration guide shows worker.start() before rendering or bootstrapping request-making code. In Node, the Node integration guide uses server.listen() and a teardown lifecycle. If the app fetches during module import before setup, the mock arrives too late.
How are handlers selected and overridden?
Define a base set of request handlers for common successful responses. In one test, add a runtime handler to override checkout with a 409 response. MSW's defaults explain that runtime handlers are prepended, so the later override can match before the initial handler. This is useful for exceptional scenarios, but it creates shared state unless teardown removes the override.
The server.use API adds runtime handlers to the current server instance. If test A installs a failure handler and test B expects the initial success handler, test B may observe failure when the runtime handler remains active. This can look like a random order-dependent failure. The remedy is not to add sleeps; it is to reset the handler state after each test and make every scenario's response explicit.
An overbroad handler is a second problem. A catch-all for /api/* may shadow a specific checkout handler and make the wrong fixture appear. Log or inspect handler matches when a response surprises you. Keep handler paths and methods specific enough that a test proves the request you care about, and avoid using “any request succeeds” handlers to conceal missing API contracts.
What does an unhandled request mean?
An unhandled request is one MSW did not match to a handler. The listen API documents onUnhandledRequest: the default behavior warns and permits the request to proceed, while the error option reports the miss as an error. A passthrough may be intentional for assets or third-party calls, but in a self-contained API test it can cause live traffic or a confusing connection failure.
For the Checkout suite, choose a strict policy for application API requests. An unexpected POST /api/checkout should fail the test rather than silently reach a staging service. If tests need a deliberate passthrough, document and narrowly scope it. The point is not that error must be universal for every request; the point is that the suite's network boundary should be explicit. A warning in a busy CI log is weak evidence that every request was mocked.
| Observed symptom | First boundary to inspect | Useful evidence |
|---|---|---|
| Live API was hit | Missing or late interception | Worker/server startup, URL, method, unhandled policy |
| Wrong fixture won | Handler precedence | Initial handlers, runtime overrides, match logs |
| Only later tests fail | Shared handler state | use calls and after-each reset |
| Browser test hangs | Worker not controlling page or response never resolved | start() timing, request trace, handler result |
This is an editorial triage ledger. It turns “MSW is flaky” into a set of inspectable transitions. In an interview, the best follow-up is to choose one symptom and identify the first observable event that should have happened but did not.
What does resetHandlers() reset?
MSW's reset-handlers documentation says a no-argument reset removes runtime handlers and returns to the initial handler set. That is the usual after-each cleanup when tests use server.use(...) for scenario overrides. Passing handlers to resetHandlers(...) replaces the initial handler list; that is a different operation and should not be casually copied into every teardown.
A conventional Node lifecycle is beforeAll(() => server.listen({ onUnhandledRequest: 'error' })), afterEach(() => server.resetHandlers()), and afterAll(() => server.close()). MSW's quick start documents this pattern. The exact test-runner syntax changes between Jest and Vitest, but the ownership principle is stable: start interception before requests, clear scenario overrides after each test, close the server at suite end. A browser suite has an analogous worker lifecycle; make sure the app does not fetch before worker.start() resolves.
Resetting handlers does not magically reset everything else. A module-level cart store, fake clock, browser cache, local storage value, or database fixture can still leak between tests. Conversely, resetting modules does not reset MSW's runtime handlers. Name each mutable layer separately. If a test passes in isolation but fails after another test, compare handler state, application state, and clock state before adding retries.
How should a test express success and failure cases?
Start with a default cart response that is realistic enough to render the UI. In a success test, assert that checkout sends the expected method and payload and that the app shows the confirmation state. In a validation test, override only POST /api/checkout to return a structured client error, then assert the inline message. In a server-error test, return 503 and assert retry affordance. The test should verify the user-visible consequence of the response, not just that a handler was invoked.
If a test counts requests, attach the counter to that test or reset it explicitly. If it checks a request body, parse it in the handler and assert the fields that matter to the contract. Do not rely on a single global lastRequest variable across parallel tests. MSW controls the network response, but test isolation still requires local ownership of observations. When parallel execution is enabled, each worker process or browser context may have different setup; validate the suite's actual isolation model.
One subtle case is a component that fetches cart data on mount and a test that registers its special handler after rendering. The initial request may already have escaped or matched the default. Register the scenario handler before triggering the app action. For a sequence of responses, explicitly model the intended order rather than assuming a runtime override will be consumed once. Handler lifetime and request count are different concepts.
How do you diagnose a failed browser test?
First verify that the Service Worker is available and in scope for the page. Then verify that worker.start() completed before app startup. Check the request's full URL and method against the handler. A relative handler path may behave differently from what you expected when the app runs on another origin. Inspect whether the request is unhandled, bypassed deliberately, or matched by an earlier handler. Finally inspect the response and the app's error path.
In Node, begin with server.listen() timing and the network client used by the app. MSW's integration guide describes supported interception, but a custom transport can sit outside the boundary you assumed. Confirm the request appears in MSW's observed events or logs before blaming the UI assertion. An unhandled error should include enough request detail to add the missing handler or correct the URL. Do not “fix” it by switching all unmatched traffic to passthrough.
These steps are an editorial debugging procedure, not a claim that one MSW setting solves every flaky test. The interview signal is whether the candidate can locate the missing transition: server/worker started, handler matched, response returned, app rendered, assertion observed. Each transition can fail for a different reason.
One practical way to make this diagnosis repeatable is to add a small request audit to the test harness. Record the request method and URL, whether a handler matched, the scenario override installed by the test, and the response status. Keep payloads out of shared logs when they contain private data. When a test fails, compare the audit with the expected request sequence: cart load, checkout submission, then confirmation or error rendering. If the request never appears, inspect the app trigger and interception startup. If it appears without a match, inspect the route. If it matches and returns but the UI is wrong, the defect is downstream of MSW. This ledger is an editorial troubleshooting asset, not a required MSW feature.
The same audit helps with retries. A client may issue a second checkout request after a 503. Decide whether the mock should return a sequence of failures and successes, or always fail, based on what the test is proving. A handler that returns success after its first invocation can make the UI pass while hiding an unintended retry loop. Specify expected request count and final visible state; otherwise a green test can leave the product behavior ambiguous.
Fast interview answers
Does server.use() permanently replace an initial handler? It adds a runtime handler with matching precedence until reset or another change.
Why can an unhandled request hit a real API? The default policy warns and permits passthrough; choose an explicit strict policy for self-contained API tests.
What does no-argument resetHandlers() do? It removes runtime overrides and restores the original handler set.
Why did a browser request escape? Check worker installation/scope and whether worker.start() completed before the request.
Does resetting handlers isolate the whole test? No. Reset app state, clocks, storage, and other shared fixtures separately.