Map your app's error codes to HTTP statuses. Define once, use everywhere, let TypeScript yell at you if you typo a code.
pnpm add @mkvlrn/app-error| Export | What it does |
|---|---|
AppError<TCode> |
Error subclass with code, statusCode, status, and a serialize() method |
defineErrors(mapping) |
Takes a code → status mapping, returns throw, create, and is helpers |
InferAppError<T> |
Extracts a qualified AppError type from a defineErrors result |
import { AppError, defineErrors } from "@mkvlrn/app-error";const errors = defineErrors({
USER_NOT_FOUND: "NOT_FOUND", // 404
INVALID_INPUT: "BAD_REQUEST", // 400
UNAUTHORIZED_ACCESS: "UNAUTHORIZED", // 401
});Keys are your codes, values are status names from http-status-codes. Both sides autocomplete.
// throws — return type is never
errors.throw("USER_NOT_FOUND", "no user with that id");
// creates without throwing
const error = errors.create("INVALID_INPUT", "email is required");
error.code; // "INVALID_INPUT"
error.statusCode; // 400
error.status; // "Bad Request"try {
JSON.parse(rawBody);
} catch (cause) {
errors.throw("INVALID_INPUT", "malformed json", cause);
}if (err instanceof AppError) {
res.status(err.statusCode).json(err.serialize());
// { code: "INVALID_INPUT", message: "email is required", details: undefined }
}errors.is() narrows an unknown value to your qualified AppError type — useful in catch blocks and error filters:
if (errors.is(err)) {
// err is AppError<"USER_NOT_FOUND" | "INVALID_INPUT" | "UNAUTHORIZED_ACCESS">
res.status(err.statusCode).json(err.serialize());
}Instead of writing AppError<"USER_NOT_FOUND" | "INVALID_INPUT" | ...> by hand, use InferAppError to extract it from your definition:
type MyAppError = InferAppError<typeof errors>;
// → AppError<"USER_NOT_FOUND" | "INVALID_INPUT" | "UNAUTHORIZED_ACCESS">
function handleError(err: MyAppError) {
// err.code is narrowed to the union — no generic to qualify manually
}throw new AppError("CUSTOM_CODE", 503, "service unavailable");MIT