Skip to main content

Error filter

Error filters translate an exception into a gRPC status. @CatchError() marks the filter and applies @injectable(). @UseErrorFilter() attaches it to a service or to one RPC. Bind the filter class.

interface ErrorFilter<
TError = unknown,
TRequest = any,
TResponse = any,
TResult = any,
> {
catch(
error: TError,
request: TRequest,
response: TResponse,
): Promise<TResult> | TResult;
}

request is the @grpc/grpc-js call. response is the callback when the RPC has one, and the call otherwise.

The adapter already handles GrpcError and its subclasses. A filter for one of your own errors can throw a GrpcError. That second error is sent as the status, including for streaming RPCs.

import {
CatchError,
type ErrorFilter,
NotFoundGrpcError,
RPC,
Service,
UseErrorFilter,
} from '@inversifyjs/grpc-core';

import {
type HeroRequest,
type HeroResponse,
heroServiceDefinition,
} from './loadHeroServiceDefinition.js';

export class HeroNotFoundError extends Error {
public readonly id: string;

constructor(id: string) {
super(`Hero ${id} was not found`);

this.id = id;
}
}

@CatchError(HeroNotFoundError)
export class HeroNotFoundErrorFilter implements ErrorFilter {
public catch(error: HeroNotFoundError): never {
throw new NotFoundGrpcError(error.message);
}
}

@Service(heroServiceDefinition)
@UseErrorFilter(HeroNotFoundErrorFilter)
export class FilteredHeroService {
@RPC('GetHero')
public getHero(call: { request: HeroRequest }): HeroResponse {
if (call.request.id === 'missing') {
throw new HeroNotFoundError(call.request.id);
}

return {
name: call.request.id,
};
}
}

@Service(heroServiceDefinition)
export class UnfilteredHeroService {
@RPC('GetHero')
public getHero(call: { request: HeroRequest }): HeroResponse {
if (call.request.id === 'missing') {
throw new HeroNotFoundError(call.request.id);
}

return {
name: call.request.id,
};
}
}

FilteredHeroService uses @UseErrorFilter(). UnfilteredHeroService throws the same error for a global filter:

adapter.useGlobalFilters(HeroNotFoundErrorFilter);

Register global filters before the server handles calls. Method filters are chosen before class filters, which are chosen before global filters. The lookup walks the error's class chain, so a filter for GrpcError also sees subclasses when no filter is registered for the subclass. @CatchError() with no error class is the fallback for every other error.

@Discriminated('hero-not-found') on an error class indexes that error by a string or symbol. A filter registered for the error class is also registered for each discriminator. At each step of the class chain, a discriminator match is used before the class match.

A filter can write the status itself by calling the unary callback or emitting an error on a stream. Throwing a GrpcError keeps that logic in one place for every RPC kind.