Error Handling
Throwing an OPRA HTTP error from a handler automatically sends the correct HTTP status code and a standardized JSON error response. Any other exception is caught by the adapter and converted to a 500 Internal Server Error.
Built-in HTTP errors
All error classes are exported from @opra/common. Throw any of them from a handler — no extra wiring needed:
import {
BadRequestError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
} from '@opra/common';
async getCustomer(ctx: HttpContext) {
const customer = await this.service.findById(ctx.pathParams.id);
if (!customer) throw new NotFoundError();
return customer;
}
| Class | Status | Default code |
|---|---|---|
BadRequestError | 400 | BAD_REQUEST |
UnauthorizedError | 401 | UNAUTHORIZED |
ForbiddenError | 403 | FORBIDDEN |
PermissionError | 403 | PERMISSION_ERROR |
NotFoundError | 404 | NOT_FOUND |
MethodNotAllowedError | 405 | METHOD_NOT_ALLOWED |
NotAcceptableError | 406 | NOT_ACCEPTABLE |
ConflictError | 409 | CONFLICT |
UnprocessableEntityError | 422 | UNPROCESSABLE_ENTITY |
FailedDependencyError | 424 | FAILED_DEPENDENCY |
InternalServerError | 500 | INTERNAL_SERVER_ERROR |
All constructors share the same signatures:
// Message only
throw new BadRequestError('Field "email" is invalid');
// Structured issue
throw new ForbiddenError({ message: 'Insufficient role', code: 'ROLE_REQUIRED', details: { required: 'admin' } });
// Wrap an original error as cause
throw new InternalServerError(cause);
Error response format
Every error — whether thrown explicitly or caught from an unexpected exception — is sent as application/opra.response+json with an errors array:
{
"errors": [
{
"message": "Customer not found",
"severity": "error",
"code": "NOT_FOUND"
}
]
}
Response headers are set automatically:
HTTP/1.1 404 Not Found
Content-Type: application/opra.response+json; charset=utf-8
Cache-Control: no-cache
Each item in errors is an ErrorIssue:
| Field | Type | Description |
|---|---|---|
message | string | Human-readable description. |
severity | 'fatal' | 'error' | 'warning' | 'info' | How serious the issue is. |
code | string | undefined | Machine-readable error code. |
details | any | Arbitrary extra data. |
diagnostics | string | string[] | undefined | Technical diagnostic info. |
stack | string | undefined | Stack trace — only included in development/test environments. |
When multiple issues are present (e.g. a batch of validation failures), all of them are collected into errors.
Validation errors
OPRA automatically runs request validation before the handler is called. When validation fails, the adapter throws a BadRequestError with code: 'REQUEST_VALIDATION' and populates details with the list of individual field failures — no additional code is required in your handler.
If you run validation manually inside a handler and receive a ValidationError from valgen, wrap it yourself:
import { BadRequestError } from '@opra/common';
import { ValidationError } from 'valgen';
async create(ctx: HttpContext) {
try {
this.myValidator(await ctx.getBody());
} catch (e) {
if (e instanceof ValidationError) throw new BadRequestError(e);
throw e;
}
}
Resource-specific errors
Two higher-level error classes handle common resource conflicts with pre-populated messages:
ResourceConflictError (409)
new ResourceConflictError(resource: string, fields: string | string[], cause?: Error)
Use when a unique-key constraint is violated:
import { ResourceConflictError } from '@opra/common';
async create(ctx: HttpContext) {
try {
return await this.service.create(await ctx.getBody());
} catch (e: any) {
if (e.code === 11000) { // MongoDB duplicate key
throw new ResourceConflictError('Customer', 'email', e);
}
throw e;
}
}
Generated message: "There is already another "Customer" resource with the same values for field(s) [email]"
ResourceNotAvailableError (422)
new ResourceNotAvailableError(resource: string, keyValue?: any, cause?: Error)
Use when an operation references a related resource that doesn't exist or is no longer active:
throw new ResourceNotAvailableError('Plan', planId);
// "Resource "Plan/abc123" is not available"
Custom errors
Extend OpraHttpError to create reusable domain-specific error classes:
import { OpraHttpError } from '@opra/common';
export class RateLimitError extends OpraHttpError {
constructor(retryAfter: number) {
super({ message: 'Too many requests', code: 'RATE_LIMIT_EXCEEDED', details: { retryAfter } }, 429);
this.name = 'RateLimitError';
}
}
throw new RateLimitError(60);
// → 429 Too Many Requests, code: RATE_LIMIT_EXCEEDED, details: { retryAfter: 60 }
Wrapping third-party errors
wrapException converts any unknown error into an OpraHttpError. It inspects the error's status property (or getStatus() method) to pick the closest built-in class, defaulting to InternalServerError for unknown status codes:
import { wrapException } from '@opra/http';
async call(ctx: HttpContext) {
try {
return await this.externalService.fetch();
} catch (e) {
throw wrapException(e);
// e.status === 404 → NotFoundError
// e.status === 403 → ForbiddenError
// anything else → InternalServerError
}
}
Incoming status | Resulting class |
|---|---|
400 | BadRequestError |
401 | UnauthorizedError |
403 | ForbiddenError |
404 | NotFoundError |
405 | MethodNotAllowedError |
406 | NotAcceptableError |
422 | UnprocessableEntityError |
424 | FailedDependencyError |
| anything else | InternalServerError |