Skip to content

Error Handling

Prerequisites: npm install jaypie (includes @jaypie/errors)

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.

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

Both function call and new syntax work:

import { BadRequestError, NotFoundError } from "jaypie";
// Function call syntax (preferred)
throw BadRequestError("Missing required field");
// Constructor syntax
throw new BadRequestError("Missing required field");

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.

const error = BadRequestError("Invalid email format");
error.status; // 400
error.title; // "Bad Request"
error.name; // "JaypieError"
error.message; // "Invalid email format"
error.detail; // "Invalid email format"
error.isJaypieError; // true
error.body(); // JSON:API formatted error object

Every Jaypie error carries the name JaypieError. Use instanceof, isJaypieError(error), or error.status to discriminate, not error.name.

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"
}
]
}

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.

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");
}

Create errors from HTTP status codes:

import { jaypieErrorFromStatus } from "jaypie";
const error = jaypieErrorFromStatus(404, "Resource not found");
// Returns NotFoundError with message "Resource not found"
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
Scenario Error
Missing env variable ConfigurationError
External API failed BadGatewayError
External API timeout GatewayTimeoutError
Service in maintenance UnavailableError
Unexpected state InternalError

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 body

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();
}
}

Never include internal details in error messages:

// Bad - exposes internal details
throw InternalError(`Database error: ${dbError.message}`);
// Good - log internally, return generic message
log.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.

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()