The api-machine framework provides a comprehensive set of HTTP error classes for consistent error handling across your API.
All HTTP error classes extend from the base HTTPError class and follow a consistent constructor pattern:
new ErrorClass(message: string, options: ErrorOptions = {})Where ErrorOptions includes:
headers?: Record<string, string>- Custom HTTP headersdetails?: unknown- Additional error details for logging/debugging
Most error classes only need a message:
import {
BadRequestError,
NotFoundError,
ConflictError
} from 'api-machine';
// 400 Bad Request
throw new BadRequestError('Invalid input format');
// 404 Not Found
throw new NotFoundError('User not found');
// 409 Conflict
throw new ConflictError('Email already exists');Add contextual information via the details option:
import {
NotFoundError,
UnprocessableEntityError
} from 'api-machine';
// Include the missing resource ID
throw new NotFoundError('User not found', {
details: { userId: 123 }
});
// Include validation errors
throw new UnprocessableEntityError('Validation failed', {
details: {
errors: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'age', message: 'Must be 18 or older' }
]
}
});Add custom headers for additional metadata:
import { BadRequestError } from 'api-machine';
throw new BadRequestError('Request failed', {
headers: {
'X-Request-Id': requestId,
'X-API-Version': '2.0'
},
details: { reason: 'Invalid JSON' }
});Some error classes have extended options for setting specific HTTP headers.
import { UnauthorizedError } from 'api-machine';
// Basic usage
throw new UnauthorizedError('Invalid token');
// Custom realm (sets WWW-Authenticate header)
throw new UnauthorizedError('Invalid token', {
realm: 'API Access'
});
// Headers: { 'WWW-Authenticate': 'Bearer realm="API Access"' }
// Custom authentication scheme
throw new UnauthorizedError('Invalid credentials', {
realm: 'Admin Portal',
scheme: 'Basic'
});
// Headers: { 'WWW-Authenticate': 'Basic realm="Admin Portal"' }
// With additional details
throw new UnauthorizedError('Token expired', {
realm: 'API',
details: {
expiredAt: '2024-01-01T00:00:00Z',
tokenId: 'abc123'
}
});import { MethodNotAllowedError } from 'api-machine';
// Basic usage
throw new MethodNotAllowedError();
// With allowed methods (sets Allow header)
throw new MethodNotAllowedError('DELETE not supported', {
allowedMethods: ['GET', 'POST', 'PUT']
});
// Headers: { 'Allow': 'GET, POST, PUT' }
// With details
throw new MethodNotAllowedError('Method not supported', {
allowedMethods: ['GET', 'POST'],
details: { attemptedMethod: 'DELETE', path: '/api/users/1' }
});import { TooManyRequestsError } from 'api-machine';
// Basic usage
throw new TooManyRequestsError();
// With retry-after (sets Retry-After header)
throw new TooManyRequestsError('Rate limit exceeded', {
retryAfter: 60 // seconds
});
// Headers: { 'Retry-After': '60' }
// With rate limit details
throw new TooManyRequestsError('Too many requests', {
retryAfter: 120,
details: {
limit: 100,
current: 150,
resetAt: '2024-01-01T12:00:00Z'
}
});import { UnsupportedMediaTypeError } from 'api-machine';
// Basic usage
throw new UnsupportedMediaTypeError();
// With accepted types (sets Accept header)
throw new UnsupportedMediaTypeError('Invalid content type', {
acceptedTypes: ['application/json', 'application/xml']
});
// Headers: { 'Accept': 'application/json, application/xml' }
// With received content type
throw new UnsupportedMediaTypeError('Unsupported media type', {
acceptedTypes: ['application/json'],
details: { receivedType: 'text/plain' }
});import { RangeNotSatisfiableError } from 'api-machine';
// Basic usage
throw new RangeNotSatisfiableError();
// With content range (sets Content-Range header)
throw new RangeNotSatisfiableError('Invalid range', {
contentRange: 'bytes */1000'
});
// Headers: { 'Content-Range': 'bytes */1000' }
// With range details
throw new RangeNotSatisfiableError('Range not satisfiable', {
contentRange: 'bytes */5000',
details: { requestedRange: 'bytes=6000-7000' }
});import { ProxyAuthenticationRequiredError } from 'api-machine';
// Default realm
throw new ProxyAuthenticationRequiredError();
// Headers: { 'Proxy-Authenticate': 'Basic realm="Proxy"' }
// Custom realm
throw new ProxyAuthenticationRequiredError('Proxy auth required', {
realm: 'Corporate Proxy'
});
// Headers: { 'Proxy-Authenticate': 'Basic realm="Corporate Proxy"' }import { UpgradeRequiredError } from 'api-machine';
// Default protocol
throw new UpgradeRequiredError();
// Headers: { 'Upgrade': 'TLS/1.0' }
// Custom protocol
throw new UpgradeRequiredError('Please upgrade', {
upgradeProtocol: 'HTTP/2.0'
});
// Headers: { 'Upgrade': 'HTTP/2.0' }| Class | Status | Default Message |
|---|---|---|
BadRequestError |
400 | Bad Request |
UnauthorizedError |
401 | Unauthorized |
PaymentRequiredError |
402 | Payment Required |
ForbiddenError |
403 | Forbidden |
NotFoundError |
404 | Not Found |
MethodNotAllowedError |
405 | Method Not Allowed |
NotAcceptableError |
406 | Not Acceptable |
ProxyAuthenticationRequiredError |
407 | Proxy Authentication Required |
RequestTimeoutError |
408 | Request Timeout |
ConflictError |
409 | Conflict |
GoneError |
410 | Gone |
LengthRequiredError |
411 | Length Required |
PreconditionFailedError |
412 | Precondition Failed |
PayloadTooLargeError |
413 | Payload Too Large |
URITooLongError |
414 | URI Too Long |
UnsupportedMediaTypeError |
415 | Unsupported Media Type |
RangeNotSatisfiableError |
416 | Range Not Satisfiable |
ExpectationFailedError |
417 | Expectation Failed |
ImATeapotError |
418 | I'm a teapot |
MisdirectedRequestError |
421 | Misdirected Request |
UnprocessableEntityError |
422 | Unprocessable Entity |
LockedError |
423 | Locked |
FailedDependencyError |
424 | Failed Dependency |
TooEarlyError |
425 | Too Early |
UpgradeRequiredError |
426 | Upgrade Required |
PreconditionRequiredError |
428 | Precondition Required |
TooManyRequestsError |
429 | Too Many Requests |
RequestHeaderFieldsTooLargeError |
431 | Request Header Fields Too Large |
UnavailableForLegalReasonsError |
451 | Unavailable For Legal Reasons |
import { ApiRequest, ApiResponse, GetEndpoint } from 'api-machine';
import { NotFoundError, ForbiddenError } from 'api-machine';
export class GetUserEndpoint extends GetEndpoint {
override path = '/:id';
async handle(request: ApiRequest, response: ApiResponse) {
const userId = parseInt(request.params['id'], 10);
// Check permissions
if (!request.user.canViewUser(userId)) {
throw new ForbiddenError('Access denied', {
details: {
userId,
requiredPermission: 'users:read'
}
});
}
// Find user
const user = await this.userRepo.findById(userId);
if (!user) {
throw new NotFoundError('User not found', {
details: { userId }
});
}
return user;
}
}The server automatically formats HTTPError instances into JSON responses:
{
"error": "NotFoundError",
"message": "User not found",
"timestamp": "2024-11-08T12:34:56.789Z",
"options": {
"details": {
"userId": 123
}
}
}HTTP headers from the error are automatically set in the response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="API"
Content-Type: application/json
{
"error": "UnauthorizedError",
"message": "Invalid token",
"timestamp": "2024-11-08T12:34:56.789Z",
"options": {}
}
Extend HTTPError to create custom error classes:
import { HTTPError } from 'api-machine';
import { HttpErrorOptions } from 'api-machine';
export interface MyCustomErrorOptions extends HttpErrorOptions {
errorCode?: string;
retryable?: boolean;
}
export class MyCustomError extends HTTPError {
public readonly errorCode?: string;
public readonly retryable?: boolean;
constructor(
message: string,
options: MyCustomErrorOptions = {}
) {
super(message, options);
this.errorCode = options.errorCode;
this.retryable = options.retryable ?? false;
}
public override getStatusCode(): number {
return 400; // Your status code
}
// Override getResponseJson to include custom fields
public override getResponseJson() {
const json = super.getResponseJson();
return {
...json,
errorCode: this.errorCode,
retryable: this.retryable,
};
}
}
// Usage
throw new MyCustomError('Something went wrong', {
errorCode: 'CUSTOM_001',
retryable: true,
details: { /* ... */ }
});- Use Specific Error Classes: Choose the most appropriate error class for your situation
- Include Details: Add contextual information via
detailsfor debugging and logging - Override Default Headers: User-provided headers always take precedence over default headers
- Consistent Messages: Use clear, user-friendly error messages
- Don't Leak Secrets: Avoid exposing sensitive information in error messages or details
// Good
throw new NotFoundError('Resource not found', {
details: { resourceType: 'user', resourceId: id }
});
// Bad - exposes internal details
throw new NotFoundError('User with email john@example.com not found in database users table');
// Bad - exposes sensitive data
throw new UnauthorizedError('Invalid token', {
details: { token: request.headers.authorization } // Don't expose tokens!
});
// Override default headers
throw new UnauthorizedError('Custom auth', {
realm: 'API', // Would set 'Bearer realm="API"'
headers: {
'WWW-Authenticate': 'Custom scheme' // This overrides the default
}
});