Error Handling
Prerequisites: npm install jaypie (includes @jaypie/errors)
Overview
Section titled “Overview”Jaypie provides typed error classes that map to HTTP status codes and format as JSON:API errors.
Never throw vanilla Error in Jaypie applications. Always use Jaypie error classes.
Quick Reference
Section titled “Quick Reference”| Error Class | Status | Use Case |
|---|---|---|
BadRequestError |
400 | Invalid input, missing required fields |
UnauthorizedError |
401 | Authentication required or failed |
ForbiddenError |
403 | Permission denied |
NotFoundError |
404 | Resource not found |
MethodNotAllowedError |
405 | HTTP method not supported |
GoneError |
410 | Resource permanently deleted |
TeapotError |
418 | Easter egg |
TooManyRequestsError |
429 | Rate limited |
InternalError |
500 | Generic server error |
ConfigurationError |
500 | Application misconfiguration |
NotImplementedError |
400 | Feature not implemented |
BadGatewayError |
502 | Upstream service error |
UnavailableError |
503 | Service unavailable |
GatewayTimeoutError |
504 | Upstream timeout |
RejectedError |
403 | Request rejected before processing |
Throwing Errors
Section titled “Throwing Errors”Both function call and new syntax work:
import { BadRequestError, NotFoundError } from "jaypie";
// Function call syntax (preferred)throw BadRequestError("Missing required field");
// Constructor syntaxthrow new BadRequestError("Missing required field");Preserving the Cause
Section titled “Preserving the Cause”Pass the caught error as cause when rethrowing:
import { ConfigurationError } from "jaypie";
try { await getSecret(name);} catch (error) { throw new ConfigurationError("Could not get or parse secret", { cause: error, });}cause reaches error.cause unchanged, so classification that walks a cause
chain sees through the Jaypie wrapper. An error constructed without the option
has no cause property, matching native Error.
Error Properties
Section titled “Error Properties”const error = BadRequestError("Invalid email format");
error.status; // 400error.title; // "Bad Request"error.name; // "JaypieError"error.message; // "Invalid email format"error.detail; // "Invalid email format"error.isJaypieError; // trueerror.body(); // JSON:API formatted error objectEvery Jaypie error carries the name JaypieError. Use instanceof,
isJaypieError(error), or error.status to discriminate, not error.name.
JSON:API Format
Section titled “JSON:API Format”Jaypie errors format as JSON:API error objects:
const error = NotFoundError("User not found");error.body();Returns:
{ "errors": [ { "status": 404, "title": "Not Found", "detail": "User not found" } ]}Checking Error Types
Section titled “Checking Error Types”instanceof identifies a specific error class:
import { NotFoundError } from "jaypie";
try { await getUser(id);} catch (error) { if (error instanceof NotFoundError) { return null; } throw error;}The check holds across the ESM and CommonJS builds, so an error raised inside a
CommonJS package matches in an ESM package, and it holds when two copies of
@jaypie/errors are installed. Class identity does not: compare with
instanceof, never error.constructor === NotFoundError.
Type Guard
Section titled “Type Guard”Use isJaypieError to safely check error type:
import { isJaypieError } from "jaypie";
try { await riskyOperation();} catch (error) { if (isJaypieError(error)) { // Safe to access .status, .body() return res.status(error.status).json(error.body()); } // Handle non-Jaypie errors throw InternalError("Unexpected error");}Dynamic Error Creation
Section titled “Dynamic Error Creation”Create errors from HTTP status codes:
import { jaypieErrorFromStatus } from "jaypie";
const error = jaypieErrorFromStatus(404, "Resource not found");// Returns NotFoundError with message "Resource not found"When to Use Each Error
Section titled “When to Use Each Error”Client Errors (4xx)
Section titled “Client Errors (4xx)”| Scenario | Error |
|---|---|
| Missing required field | BadRequestError |
| Invalid field format | BadRequestError |
| Missing auth token | UnauthorizedError |
| Invalid auth token | UnauthorizedError |
| User lacks permission | ForbiddenError |
| Resource doesn’t exist | NotFoundError |
| Wrong HTTP method | MethodNotAllowedError |
| Resource was deleted | GoneError |
| Rate limit exceeded | TooManyRequestsError |
Server Errors (5xx)
Section titled “Server Errors (5xx)”| Scenario | Error |
|---|---|
| Missing env variable | ConfigurationError |
| External API failed | BadGatewayError |
| External API timeout | GatewayTimeoutError |
| Service in maintenance | UnavailableError |
| Unexpected state | InternalError |
Error Handling Pattern
Section titled “Error Handling Pattern”In Handlers
Section titled “In Handlers”Jaypie handlers automatically catch and format errors:
import { expressHandler, NotFoundError } from "jaypie";
export default expressHandler(async (req, res) => { const user = await db.users.findById(req.params.id); if (!user) throw NotFoundError("User not found"); return { data: user };});// NotFoundError automatically returns 404 with JSON:API bodyWrapping External Errors
Section titled “Wrapping External Errors”Convert external service errors to Jaypie errors:
import { BadGatewayError, log } from "jaypie";
async function callExternalApi(data) { try { return await externalService.call(data); } catch (error) { log.error("External API failed"); log.var({ error: error.message }); throw BadGatewayError(); }}Don’t Include Sensitive Data
Section titled “Don’t Include Sensitive Data”Never include internal details in error messages:
// Bad - exposes internal detailsthrow InternalError(`Database error: ${dbError.message}`);
// Good - log internally, return generic messagelog.error("Database query failed");log.var({ error: dbError.message });throw InternalError();Handlers enforce this: an error message reaches the logs, never a response body. An error message is therefore not a way to tell the caller anything. Return a normal response body when the caller needs specifics.
Testing Errors
Section titled “Testing Errors”Use @jaypie/testkit custom matchers:
import { matchers } from "@jaypie/testkit";expect.extend(matchers);
it("throws NotFoundError for missing user", () => { expect(() => getUser("invalid-id")).toThrowNotFoundError();});
it("throws any Jaypie error", () => { expect(() => riskyOperation()).toThrowJaypieError();});Available matchers:
toThrowJaypieError()toThrowBadRequestError()toThrowUnauthorizedError()toThrowForbiddenError()toThrowNotFoundError()toThrowInternalError()toThrowConfigurationError()toThrowBadGatewayError()toThrowUnavailableError()
Related
Section titled “Related”- Handler Lifecycle - How handlers process errors
- Logging - Logging errors appropriately
- @jaypie/errors - Full API reference
- Testing - Testing error conditions