Error Handling Patterns
Problem
HTTP errors (4xx, 5xx), network failures, and timeouts need different recovery strategies. You want consistent handling across all call sites without repeating the same try/catch branches.
Solution
Status-code branching
ts
import { HttpError } from '@vielzeug/fetchit';
try {
await api.get('/users/1');
} catch (err) {
if (HttpError.is(err, 404)) return null;
if (HttpError.is(err, 401)) return redirectToLogin();
if (HttpError.is(err, 403)) return showForbidden();
if (HttpError.is(err) && err.kind === 'timeout') return toast.error('Request timed out');
if (HttpError.is(err) && err.kind === 'abort') return; // user canceled
if (HttpError.is(err)) throw new Error(`Unexpected ${err.status}: ${err.url}`);
throw err; // re-throw non-HTTP errors
}Global error logger
ts
const api = createApi({ baseUrl: 'https://api.example.com' });
api.use(async (ctx, next) => {
try {
return await next(ctx);
} catch (error) {
Sentry.captureException(error, {
extra: { method: ctx.init.method, url: ctx.url },
});
throw error;
}
});Mutation error state
ts
const mutation = createMutation((input: number, signal: AbortSignal) => api.delete(`/users/${input}`, { signal }));
mutation.subscribe((state) => {
if (state.status === 'error') {
// State is observable — no need for try/catch in UI
toast.error(state.error!.message);
mutation.reset();
}
});
mutation.mutate(1).catch(() => {}); // error is surfaced via state, not thrownPitfalls
- Fetchit distinguishes HTTP errors (4xx/5xx with a response) from network errors (no response). Treating them identically hides the root cause — check
response.okbefore accessing the body. - A
500response with a JSON error body arrives in the success path unless you throw in theonResponseinterceptor. Do not assume an HTTP error is a thrown exception. - Retrying on all errors wastes resources on 401 (wrong credentials) or 422 (validation failure). Limit retries to network errors and 5xx status codes.