Jest · Unit tests that survive refactors
How do you test errors and failure paths in Jest?
Short answer
Assert thrown errors with toThrow, and match the message or type rather than the full stack. For async failures use rejects, and test the user-facing error state as well.
Failure paths are part of the contract. A function that throws a typed error, a UI that shows a retry message, and a retry that gives up after three attempts all deserve tests. Matching only that something threw is weak because any unrelated bug also throws.
test("rejects an invalid total", () => {
expect(() => createInvoice({ total: -1 })).toThrow(ValidationError);
expect(() => createInvoice({ total: -1 })).toThrow("total must be positive");
});
test("shows a retry message when the API fails", async () => {
server.use(
http.get("/api/items", () => new HttpResponse(null, { status: 500 })),
);
render(<ItemList />);
expect(await screen.findByRole("alert")).toHaveTextContent("Try again");
});What to assert on an error
- The error type, using the constructor when you have a custom class
- A stable part of the message, not the full stack
- The user-visible error state and any recovery action
- That state was not partially mutated after the failure
- That a retry or fallback ran the expected number of times
For async code, rejects.toThrow handles a rejected promise. If the function throws synchronously, use toThrow with a wrapper function instead, because there is no promise to await. Passing a function to toThrow is required; expect(fn()).toThrow() throws before the matcher runs.
Follow-ups you should expect
Interviewers use these to check whether the first answer was understanding or memorisation.
- 1Why is expect(fn()).toThrow() wrong?
- 2How do you assert on a custom error class?
- 3How do you test a retry that eventually gives up?
- 4Why should you test the UI error state separately from the thrown error?
Say it in the interview
I test failure paths as deliberately as success paths. For sync code I pass a wrapper function to toThrow and match the error type or a stable part of the message. For async code I use rejects. I also render the UI and assert on the visible error and recovery action, because a correct thrown error can still produce a blank screen. I never assert on stack traces.
Use this as a model, not a script. Saying it in your own words is what makes it survive follow-up pressure.
Related questions
Reading is step one
Now say it out loud.
Understanding an answer and delivering it under pressure are different skills. Rehearse this topic in a live Jest mock interview and get scored feedback on depth, clarity and confidence.