Skip to main content

Error Filter

Error filters allow you to catch and process errors that occur during request handling. They provide a centralized way to transform application errors into appropriate HTTP responses, keeping your controllers clean and focused on business logic.

Error filter and injectable decorators

You don't need to add @injectable(). The @CatchError() decorator already applies it for you.

How Error Filters Work​

Error filters are classes that implement the ErrorFilter interface and are decorated with @CatchError(). When an error occurs during request processing, Inversify HTTP looks for an error filter that can handle that error and delegates the error handling to it. A filter can match the error's class or a discriminator declared on that class. Inversify HTTP also registers a default error filter for the ErrorHttpResponse errors it provides. When several filters could handle the same error, see error filter precedence.

The error filter can then:

  • Transform the error into an appropriate HTTP response
  • Log the error for debugging purposes
  • Add context or sanitize error messages
  • Return custom error responses based on the error type

Basic Example​

Here's a complete example showing how to create a custom error, an error filter to handle it, and a controller that uses the error filter:

Custom Error​

First, create a custom error class:

export class InvalidOperationError extends Error {
constructor(message: string = 'Invalid operation', options?: ErrorOptions) {
super(`[InvalidOperationError]: ${message}`, options);
}
}

Error Filter​

Create an error filter that catches the custom error and returns an appropriate HTTP response:

@CatchError(InvalidOperationError)
export class InvalidOperationErrorFilter implements ErrorFilter<InvalidOperationError> {
public catch(error: InvalidOperationError): void {
throw new UnprocessableEntityHttpResponse(
{ message: error.message },
error.message,
{
cause: error,
},
);
}
}
universal error filter and infinite loops

When creating a universal error filter (one that catches anything), be cautious about throwing HttpResponse errors from within the filter. This can lead to infinite loops if the filter catches its own thrown errors. Instead, consider returning the response directly.

Controller​

Apply the error filter to a controller using the @UseErrorFilter() decorator:

@Controller('/products')
@UseErrorFilter(InvalidOperationErrorFilter)
export class ProductController {
@Get('/:id/validate')
public async validateProduct(): Promise<void> {
throw new InvalidOperationError('Product validation failed');
}
}

Matching errors by discriminator​

Error filter lookup matches an error by its constructor. A package that is loaded twice can create two constructors for the same error class, and an instance of one copy fails instanceof against the other. @Discriminated() stores a string or symbol on the error class so lookup can still find the filter. Import it from @inversifyjs/http-core.

function Discriminated(discriminator: string | symbol): ClassDecorator

A filter registered with @CatchError(ErrorClass) is also registered for every discriminator on that class. Lookup starts at the thrown error's class and walks toward its parents. At each class, a discriminator match is used before a match on the class itself. Each class keeps the discriminators applied to it.

Error hierarchies​

Give a subclass its own discriminator when that subclass needs its own filter. Lookup reads the thrown class's discriminators before it matches that class, then repeats the check on each parent.

OutOfStockError extends WarehouseError. OutOfStockErrorFilter handles OutOfStockError. WarehouseErrorFilter handles WarehouseError. ClosedWarehouseError extends WarehouseError and has no filter of its own, so WarehouseErrorFilter handles it too.

@Discriminated('warehouse')
export class WarehouseError extends Error {}

@Discriminated('out-of-stock')
export class OutOfStockError extends WarehouseError {
constructor(sku: string) {
super(`Product ${sku} is out of stock`);
}
}

export class ClosedWarehouseError extends WarehouseError {}
@CatchError(WarehouseError)
export class WarehouseErrorFilter implements ErrorFilter<WarehouseError> {
public catch(error: WarehouseError): void {
throw new BadRequestHttpResponse(
{ message: error.message },
error.message,
{
cause: error,
},
);
}
}

@CatchError(OutOfStockError)
export class OutOfStockErrorFilter implements ErrorFilter<OutOfStockError> {
public catch(error: OutOfStockError): void {
throw new ConflictHttpResponse({ message: error.message }, error.message, {
cause: error,
});
}
}
@Controller('/warehouse')
@UseErrorFilter(OutOfStockErrorFilter, WarehouseErrorFilter)
export class WarehouseController {
@Get('/out-of-stock')
public outOfStock(): void {
throw new OutOfStockError('SKU-1');
}

@Get('/closed')
public closed(): void {
throw new WarehouseError('Warehouse is closed');
}

@Get('/maintenance')
public maintenance(): void {
throw new ClosedWarehouseError('Warehouse is closed for maintenance');
}
}

GET /warehouse/out-of-stock responds with 409 Conflict. GET /warehouse/closed and GET /warehouse/maintenance respond with 400 Bad Request.

Default error filter​

Inversify HTTP registers a global error filter for ErrorHttpResponse, the base class of every error response the framework provides (BadRequestHttpResponse, NotFoundHttpResponse, InternalServerErrorHttpResponse, and so on). When a handler, guard, pipe, middleware, or interceptor throws one of these errors, the default filter sends it as the response, using the error's status code, body, and headers. That is why throwing new NotFoundHttpResponse(...) works without registering any error filter.

The default filter follows the same precedence rules as any other global filter, because it is registered for the ErrorHttpResponse class.

the default error filter runs before catch-all and Error filters

The lookup walks the thrown error's class chain, and the default filter is registered for ErrorHttpResponse. A thrown NotFoundHttpResponse is handled by the default filter before the lookup reaches Error or the catch-all (@CatchError() without an error class). A catch-all or Error filter, whether global or attached to a route, does not receive ErrorHttpResponse errors.

To handle these errors yourself, use one of these options:

  • Register a filter for a more specific class, such as @CatchError(NotFoundHttpResponse). It runs instead of the default filter for that response class.
  • Register a filter for ErrorHttpResponse itself. Attached to a route with @UseErrorFilter(), it replaces the default filter for that route. Registered with useGlobalFilters(), it replaces the default filter for every route. The filter is then responsible for writing the response, as described in returning responses from error filters.

Errors that are not ErrorHttpResponse instances are not affected by the default filter. If no filter handles them, the adapter responds with 500 Internal Server Error.

Returning responses from error filters​

Error filters can directly manipulate the HTTP response using the adapter-specific request and response objects. This approach is particularly useful for global error filters that need to handle all types of errors without causing infinite loops.

Examples by Adapter​

@CatchError()
export class GlobalErrorFilter implements ExpressErrorFilter {
readonly #logger: Logger;

constructor() {
this.#logger = new ConsoleLogger('GlobalErrorFilter');
}

public catch(
err: unknown,
_request: express.Request,
response: express.Response,
): void {
if (isHttpResponse(err)) {
this.#logger.http(
`HttpResponse error: ${JSON.stringify(err, Object.getOwnPropertyNames(err))}`,
);

response.status(err.statusCode).send(err.body);

return;
}

this.#logger.error(
`Unhandled error: ${JSON.stringify(err, Object.getOwnPropertyNames(err))}`,
);

const internalServerErrorStatusCode: number = 500;

response.status(internalServerErrorStatusCode).send({
error: 'Internal Server Error',
message: 'Unhandled error',
statusCode: internalServerErrorStatusCode,
});
}
}

In these examples:

  • The error filter implements the adapter-specific ErrorFilter type (e.g., ExpressErrorFilter, FastifyErrorFilter)
  • A logger is instantiated using ConsoleLogger from @inversifyjs/logger to log errors
  • The isHttpResponse() utility checks if the caught error is an HttpResponse instance
  • If it's an HttpResponse, the filter logs it with .http() level and sends the response with the original status code and body
  • For generic errors, the filter logs them with .error() level using JSON.stringify(err, Object.getOwnPropertyNames(err)) to properly serialize the error (see why this is necessary)
  • The filter returns a 500 Internal Server Error response for generic errors
  • The filter is registered globally using adapter.useGlobalFilters(GlobalErrorFilter)

This pattern ensures that:

  • Generic JavaScript errors are caught, logged, and transformed into proper HTTP responses
  • Error details are properly serialized for logging (Error objects have non-enumerable properties that require special handling)
  • Error handling and logging are centralized and consistent across your application
error responses reach the default error filter first

ErrorHttpResponse subclasses are handled by the default error filter before the lookup reaches a catch-all filter like GlobalErrorFilter.To customize ErrorHttpResponse errors, register a filter for ErrorHttpResponse.

Attaching error filters​

Use the UseErrorFilter decorator at the controller level (applies to all routes) or at the method level (applies to one route).

Global error filters can be registered using the InversifyHttpAdapter.

Error filter precedence​

When an error is thrown, Inversify HTTP picks the error filter by applying these rules in order:

  1. Error class first. Lookup starts at the thrown error's class and walks toward its parents. It ends with the catch-all filter (@CatchError() without an error class). The first error class that has a filter wins. At each class, a discriminator match is used before a match on the class itself.
  2. Route filters before global filters. For the same error class, a filter attached with @UseErrorFilter() wins over a global filter registered with useGlobalFilters().
  3. Method filters before controller filters. For the same error class, a method-level filter wins over a controller-level filter.
a global filter can win over a route filter

Rule 1 is applied before rules 2 and 3. A global filter for TypeError handles a thrown TypeError even when the route has a filter for Error, because the lookup finds a filter for TypeError before it reaches Error. The route filter handles only the errors that no filter matches earlier in the lookup. To make a route filter take over, attach it for the same error class as the global filter, or for a more specific one. The default error filter for ErrorHttpResponse errors is an example of this.

overriding error filters for the same error type

Only one error filter handles a given error type at each level. When more than one error filter catches the same error type at the same level, the last error filter applied overrides the previous ones:

  • Within a single @UseErrorFilter() call, error filters are applied in the order they are listed.
  • Global error filters are applied in the order they are registered with useGlobalFilters().
  • Controller-level filters are applied before method-level filters, so a method-level filter overrides a controller-level filter for the same error type. This is expected and is not reported.

A warning is logged when an error filter overrides a different error filter attached at the same level.