error-handling-test
Error Handling Test Skill
Discovery
Before writing any tests, scan the target code for:
- Explicit throws —
throw new Error(...), custom error classes, re-throws - Implicit throws — array/object destructuring on null, property access on undefined, calling non-functions
- Async failure surfaces —
Promise.reject,asyncfunctions thatthrow, unhandled branches in.then()chains - Fallbacks — default values, catch blocks that return something, optional chaining with fallback (
?? default), try/catch that swallows errors silently - Error propagation — does the function rethrow, wrap, or consume the error? Each behavior needs a different assertion
Non-Obvious Patterns to Cover
1. Assert the error type and message, not just that something throws
// WEAK — only verifies something threw
expect(() => fn()).toThrow();
// STRONG — verifies what threw and why
expect(() => fn(null)).toThrow(ValidationError);
expect(() => fn(null)).toThrow('userId is required');
For custom error classes, assert the .code or .statusCode property too if it exists.
2. Async: use rejects not try/catch
// WRONG — silent pass if promise resolves instead of rejects
try {
await riskyFn();
fail('should have thrown');
} catch (e) { ... }
// CORRECT
await expect(riskyFn()).rejects.toThrow(NetworkError);
await expect(riskyFn()).rejects.toMatchObject({ code: 'ECONNREFUSED' });
3. Verify fallback values, not just absence of errors
When a function catches internally and returns a default, test the fallback explicitly:
// Don't just test that it doesn't throw
const result = await fetchWithFallback('/bad-url');
expect(result).toEqual(DEFAULT_CONFIG); // assert the fallback was used
4. Spy on error consumers — catch blocks that log or emit
If error handling calls logger.error, Sentry.captureException, or emits an event, assert those side effects:
const spy = jest.spyOn(logger, 'error');
await riskyOperation();
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('fetch failed'),
expect.any(Error)
);
5. Error boundary / retry exhaustion
For retry logic, don't just mock a single failure — mock n consecutive failures to verify exhaustion behavior:
fetchMock.mockRejectedValue(new Error('timeout')).mockRejectedValueOnce(...);
// or use mockRejectedValue for all calls then assert maxRetries hit
expect(fetchMock).toHaveBeenCalledTimes(3); // assert retry count
await expect(result).rejects.toThrow('Max retries exceeded');
6. Partial failure in parallel operations
Promise.allSettled vs Promise.all behave differently — test accordingly:
// for Promise.all — one rejection should reject the whole call
mockFn.mockResolvedValueOnce('ok').mockRejectedValueOnce(new Error('fail'));
await expect(Promise.all([mockFn(), mockFn()])).rejects.toThrow('fail');
// for Promise.allSettled — partial failure should not throw
const results = await Promise.allSettled([mockFn(), mockFn()]);
expect(results[1].status).toBe('rejected');
7. Error swallowing — the hidden failure mode
If a catch block does nothing (or only logs), write a test that confirms the caller receives the expected neutral result — not that an error was thrown:
// The function eats the error; caller gets undefined/null/empty
const result = await silentFail();
expect(result).toBeNull(); // or undefined, [], {} — whatever the contract says
8. State integrity after failure
For stateful modules, assert that state is unchanged (or rolled back) after a failed operation:
const before = store.getSnapshot();
await expect(store.update(invalidPayload)).rejects.toThrow();
expect(store.getSnapshot()).toEqual(before); // no partial mutation
Strategy by Error Surface
| Surface | Key assertion | Mock strategy |
|---|---|---|
| Sync throw | toThrow(ErrorClass) + message |
Pass invalid args directly |
| Async reject | rejects.toThrow() |
mockRejectedValue |
| Fetch / HTTP | Status code + error shape | msw handler or fetch mock returning { ok: false, status: 500 } |
| DB / IO | Connection error, query error | Mock at adapter layer, not at fetch |
| Silent catch | Fallback value assertion | Let real error occur, assert return value |
| Event-based errors | Event listener spy | emitter.emit('error', ...) or trigger condition |
What Not to Do
- Don't
console.login tests to "verify" an error happened — use spies - Don't use
try/catchin async tests — userejects - Don't test the mock — if you mock
throwand then assertthrow, you've tested nothing; ensure the production code path is exercised - Don't group all error cases in one test — one assertion per test makes failures legible
Output Format
Produce test blocks grouped by the type of error surface (thrown errors, rejected promises, fallbacks, side effects), with one it/test per distinct failure condition. Include a brief comment on each test block explaining what contract is being verified, not just what the code does.
More from blunotech-dev/agents
anti-purple-ui
Enforce a strict monochrome UI with a single high-contrast accent color, removing generic tech gradients and “AI-style” palettes. Use when the user wants minimal, anti-AI, or non-generic aesthetics, or says the UI looks too techy or generic.
9harmonize-whitespace
Align all spacing (padding, margins, gaps) to a consistent 4pt/8pt grid. Use when spacing feels off, inconsistent, cramped, or unbalanced, or when the user asks for a spacing scale or alignment fix.
9typographic-hierarchy
Improve typography by adjusting font sizes, weights, spacing, and contrast to create clear visual hierarchy and readability. Use when text feels flat, unstructured, or when the user asks to refine headings, type scale, or overall readability.
6micro-interaction-adder
Add polished CSS micro-interactions like hover effects, transitions, and feedback states to improve UI feel. Use when the user asks for animations, better UX, or when the interface feels static, plain, or unresponsive.
4consistent-border-radius
Normalizes rounded corners across a file so buttons, inputs, cards, modals, badges, and all UI elements share the exact same curvature. Use this skill whenever the user mentions inconsistent border radii, wants to unify rounded corners, asks to make UI elements look more cohesive, or says things like "make the corners match", "fix the rounding", "unify border radius", "standardize my rounded corners", or "buttons and cards don't match". Also trigger when the user pastes a CSS/HTML/JSX/TSX file and asks for a design consistency pass, border radius is one of the first things to normalize.
4component-split
Analyze a component and determine when and how to split it based on size, responsibility, and reuse signals, producing a refactored structure with clear boundaries. Use when users share large, mixed-concern, or hard-to-test components, or ask about splitting, refactoring, or improving component architecture.
3