Skip to main content

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;
}
ClassStatusDefault code
BadRequestError400BAD_REQUEST
UnauthorizedError401UNAUTHORIZED
ForbiddenError403FORBIDDEN
PermissionError403PERMISSION_ERROR
NotFoundError404NOT_FOUND
MethodNotAllowedError405METHOD_NOT_ALLOWED
NotAcceptableError406NOT_ACCEPTABLE
ConflictError409CONFLICT
UnprocessableEntityError422UNPROCESSABLE_ENTITY
FailedDependencyError424FAILED_DEPENDENCY
InternalServerError500INTERNAL_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:

FieldTypeDescription
messagestringHuman-readable description.
severity'fatal' | 'error' | 'warning' | 'info'How serious the issue is.
codestring | undefinedMachine-readable error code.
detailsanyArbitrary extra data.
diagnosticsstring | string[] | undefinedTechnical diagnostic info.
stackstring | undefinedStack 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 statusResulting class
400BadRequestError
401UnauthorizedError
403ForbiddenError
404NotFoundError
405MethodNotAllowedError
406NotAcceptableError
422UnprocessableEntityError
424FailedDependencyError
anything elseInternalServerError

Sending Responses · Methods