Files
What you'll learn
- Throw RpcException for an error the caller should see, and know what it becomes on the wire: an object as it is, a string as { status: 'error', message }
- Know why any other error, an HttpException included, reaches the caller only as 'Internal server error'
- Write an RpcExceptionFilter whose catch() returns an erroring Observable, and bind it with @UseFilters
- Translate reply errors into HTTP statuses at the gateway with an RxJS operator, including a 502 for failures the service did not explain
Errors across the wire
In one HTTP app, a NotFoundException thrown anywhere becomes a 404 with a readable body, and that is the end of it. Split the app in two and the error has to travel. It is thrown in the cats service, serialized, carried over TCP, and handed to the gateway, which must still answer the visitor with the right status. Nothing about that journey is automatic. This lesson follows an error from the throw in the service to the status the visitor sees.
What a microservice sends back
When a message handler throws, Nest's RPC exception layer decides what the caller receives, and it knows exactly one kind of error: RpcException.
throw new RpcException({ status: 403, message: 'This shelter is closed' });
// the caller receives { status: 403, message: 'This shelter is closed' }
throw new RpcException('This shelter is closed');
// the caller receives { status: 'error', message: 'This shelter is closed' }
An object is sent as it is, so you choose its shape; the status inside is only a number both sides agreed on. A string is wrapped with status: 'error'. Every other error is sent as { status: 'error', message: 'Internal server error' }, so the caller cannot tell what went wrong. That includes your own Error subclasses and Nest's own NotFoundException. HTTP exceptions mean nothing to a transport. A plain Error is at least logged in the service's console. An HttpException is not even logged: it disappears into "Internal server error" with no trace anywhere.
Exception filters for messages
Services keep throwing what makes sense for the domain, and a filter translates at the edge, as on HTTP. The difference is that a microservice filter's catch() does not write a response. It returns an Observable, and the error that Observable carries is what the caller receives:
@Catch(ShelterClosedError)
export class ShelterClosedFilter implements RpcExceptionFilter<ShelterClosedError> {
catch(exception: ShelterClosedError, host: ArgumentsHost): Observable<never> {
return throwError(() => ({ status: 403, message: exception.message }));
}
}
RpcExceptionFilter comes from @nestjs/common. Bind the filter as on HTTP: @UseFilters(new ShelterClosedFilter()) on a handler or on the controller. If catch() returns a value instead of an error (of(...)), the caller receives it as a successful reply. To adjust Nest's default handling instead of replacing it, extend BaseRpcExceptionFilter and call super.catch().
At the gateway
On the gateway's side the reply's Observable errors with that plain object, not an Error, and not an HttpException. Nest's HTTP layer does not know it, so the visitor gets a 500 and the console logs Object(2) { status: 404, message: "Cat 9 not found" }. The gateway has to translate. An RxJS operator does it once for every call:
export function toProblem<T>(): MonoTypeOperatorFunction<T> {
return catchError((err) => throwError(() => new HttpException(err.message, err.status)));
}
this.shelters.send({ cmd: 'open' }, id).pipe(toProblem());
A gateway also meets failures the service never described: a string such as There is no matching message handler defined in the remote service., or { status: 'error', message: 'Internal server error' }. Neither carries a number. 502 Bad Gateway is the honest answer: the server behind the gateway failed. 500 would blame the gateway itself.
Your task
The service can rename a cat now. CatsService.rename() throws a NameTakenError (a plain Error subclass) when another cat has the name. The gateway has a PATCH /cats/:id route, and a GET /cats/:id/history route that was deployed before the service learned that pattern.
- In the service's
CatsController, an unknown id is an error the caller can act on: throw anRpcExceptioncarrying{ status: 404, message: 'Cat <id> not found' }, both for{ cmd: 'find-one' }and for{ cmd: 'rename' }. - Write
NameTakenFilter: anRpcExceptionFilterforNameTakenErrorwhose error is{ status: 409, message: <the error's message> }. Bind it to the rename handler. - In
gateway/rpc-errors.ts, writetoHttpError(). An error with a numericstatusbecomes anHttpExceptionwith that status and itsmessage. A string, or an error without a number, becomes a502whose message is the string or the error's message. Pipe everysend()in the gateway through it.GET /cats/:idno longer needs itsnullcheck.
When it fails
GET /cats/9answers 502Internal server error, and the service logged nothing: it threwNotFoundException, which a transport does not understand. ThrowRpcException.- 502 with the right message but not the right status:
new RpcException('Cat 9 not found')sends a string, which carriesstatus: 'error'. Pass an object with a numericstatus. - The rename is 502 and the service logs
ERROR [RpcExceptionsHandler] NameTakenError: There is already a cat named Tom: the filter is not bound, or@Catch()names another class. - The rename answers 200 with
{ "status": 409, ... }:catch()returned a value. ReturnthrowError(() => ...). - 500 at the gateway and
ERROR [ExceptionsHandler] Object(2) { status: 404, ... }: the reply's error reached Nest's HTTP layer untranslated. A call is missing its operator.
Remember
- Only
RpcExceptioncrosses the wire intact: an object as it is, a string as{ status: 'error', message }. - Everything else,
HttpExceptionincluded, arrives asInternal server error. - A microservice filter returns an erroring Observable; bind it with
@UseFilters(). - The gateway translates reply errors into HTTP, with 502 for failures the service did not explain.
Stuck? Show a hint
Service: throw new RpcException({ status: 404, message }) where a cat is missing; @Catch(NameTakenError) on a class implementing RpcExceptionFilter from @nestjs/common, whose catch() returns throwError(() => ({ status: 409, message })); @UseFilters(new NameTakenFilter()) on the rename handler. Gateway: toHttpError() returns catchError(err => throwError(() => new HttpException(message, status))), and every send() pipes it.
Press Run tests to start the app. Its log appears here.Graded endpoints
No error, nothing to translate: the reply passes through the operator untouched
The service throws RpcException({ status: 404, message }); the object crosses the wire as it is, and the gateway keeps its status
CatsService throws a NameTakenError, a plain domain error; the filter bound to the rename handler turns it into { status: 409, message }
The rename handler finds the cat first, so an unknown id is the same 404 as a lookup, not a crash on null
The rename succeeds and the reply is the renamed cat
The service answers a plain string, no status: the gateway says 502 Bad Gateway, the failure of the server behind it, and passes the message on
The adoption event from lesson 2 still flows
Luna is Nala now, Tom has an adopter, and a successful list passes through the operator as well