Throw like a PRO in NestJS
I would like to share my ideas on how error handling could be improved for your service. What would we like to achieve?
- Easily search and reference occurred errors
- Enrich and categorize service errors
- Expose our errors as metrics
Find a link to the full source code at the end of the story.

Table of Contents
- Step 1. Bootstrap project
- Step 2. Inject an error
- Step 3. Using NestJS HttpExceptions
- Step 4. Logging errors
- Step 5. Enriching exceptions
- Step 6. Using enriched exceptions
- Summary
Step 1. Bootstrap project
At first, as usual, let’s bootstrap an example service for us. I recommend you to use powerful nest-cli to generate the skeleton for our app:
$ npx nest new nestjs-errors
# ...
# ? Which package manager would you ❤️ to use?
# > npm
Step 2. Inject an error
Let’s throw some error straight away! In app.controller.ts change code to this:
import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
getHello(): string {
throw new Error('Oh no!');
}
}
Yes, let’s start from basics: throwing an Error
at first. I will use HTTPie as a tool for testing. Let’s see what we get as a response:
$ http http://localhost:3000HTTP/1.1 500 Internal Server Error{
"message": "Internal server error",
"statusCode": 500
}
Wow! We don’t even see the error message in response. Diving into NestJS docs you’ll find some nice hierarchy of HTTP exceptions bound to HTTP response statuses.
Step 3. Using NestJS HttpExceptions
Let’s use HttpException
from @nestjs/common
package:
import {
Controller,
Get,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Controller()
export class AppController {
@Get()
getHello(): string {
throw new HttpException(
'Oh no!',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
Test it out:
$ http http://localhost:3000HTTP/1.1 500 Internal Server Error{
"message": "Oh no!",
"statusCode": 500
}
Okay, now we have at least an error message.
Also note there are exception aliases for common HTTP statuses, e.g.:
InternalServerErrorException
,BadRequestException
, etc.Find the full list of built-in exceptions here.
If you look into the logs, you won’t find any related to the error. What a pity it would be in production! Let’s fix it up!
Step 4. Logging errors
The process of logging errors is well-described in the official docs. We can use BaseExceptionFilter
to catch all or specific errors.
Let’s create a new file exception.filter.ts:
import { Catch, ArgumentsHost, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class CustomExceptionFilter extends BaseExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
this.logger.error(JSON.stringify(exception));
super.catch(exception, host);
}
}
And tell NestJS to use it as a global filter in main.ts:
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// We will get rid of this later
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new CustomExceptionFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
Let’s try it out:
$ http http://localhost:3000HTTP/1.1 500 Internal Server Error{
"message": "Oh no!",
"statusCode": 500
}
Check out the logs:
[Nest] 18523 - 07/04/2022, 5:50:19 PM LOG [NestApplication] Nest application successfully started +2ms[Nest] 18523 - 07/04/2022, 5:51:12 PM ERROR [AllExceptionsFilter] {"response":"Oh no!","status":500,"message":"Oh no!","name":"HttpException"}
Yes! That’s exactly what we wanted to have. Now at least we can observe our exceptions.
Why haven’t we used ExceptionFilter interface in this case? Simply because at this step we’ll lose serialisation mechanism provided by NestJS. Read further on how to make our own serialiser with enriched exceptions.
But simply an error message does not provide us any details of the error that occurred. Moreover, it is not “machine-readable” enough. Let’s fix it.
Step 5. Enriching exceptions
What extra properties typical exceptions should contain? Personally, for me, it’s at least id
, domain
, andtimestamp
. But how to achieve it without losing the power of NestJS?
At first, let’s define a model for the desired exception, business.exception.ts:
import { HttpStatus } from '@nestjs/common';
export type ErrorDomain = 'users' | 'orders' | 'generic';
export class BusinessException extends Error {
public readonly id: string;
public readonly timestamp: Date;
constructor(
public readonly domain: ErrorDomain,
public readonly message: string,
public readonly apiMessage: string,
public readonly status: HttpStatus,
) {
super(message);
this.id = BusinessException.genId();
this.timestamp = new Date();
}
private static genId(length = 16): string {
const p = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return [...Array(length)].reduce(
(a) => a + p[~~(Math.random() * p.length)],
'',
);
}
}
Let’s break it down by properties:
id
contains a unique identifier that is generated when the exception is being initialised;timestamp
stores timestamp when the exception was initialised;domain
specifies which business domain this error belongs to or where it occurred;message
contains an internal message for logging (may contain private data, e.g. identifiers, exception message, stack trace, etc.);apiMessage
contains a message to be returned in the response to the user. This one is publicly exposed;status
specifies which HTTP status the service must respond with when this error occurred.
Step 6. Using enriched exceptions
In order to integrate these exceptions, we need two things: actually start using them and adapt our ExceptionFilter
to properly handle these errors.
- Let’s throw our custom error instead of the built-in exception, at app.controller.ts:
import { Controller, Get, HttpStatus } from '@nestjs/common';
import { BusinessException } from './business.exception';
@Controller()
export class AppController {
@Get()
getHello(): string {
const userId = 1;
throw new BusinessException(
'users', // Error domain
`User with id=${userId} was not found.`, // Internal message
'User not found', // API message
HttpStatus.NOT_FOUND, // HTTP status
);
}
}
2. Adapt our ExceptionFilter
to properly handle our custom exceptions, replacing BaseExceptionFilter
with ExceptionFilter
in exception.filter.ts:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import {
BusinessException,
ErrorDomain
} from './business.exception';
export interface ApiError {
id: string;
domain: ErrorDomain;
message: string;
timestamp: Date;
}
@Catch(Error)
export class CustomExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(CustomExceptionFilter.name);
catch(exception: Error, host: ArgumentsHost) {
let body: ApiError;
let status: HttpStatus;
if (exception instanceof BusinessException) {
// Straightforward handling of our own exceptions
body = {
id: exception.id,
message: exception.apiMessage,
domain: exception.domain,
timestamp: exception.timestamp,
};
status = exception.status;
} else if (exception instanceof HttpException) {
// We can extract internal message & status from NestJS errors
// Useful with class-validator
body = new BusinessException(
'generic',
exception.message,
exception.message, // Or generic message if you like
exception.getStatus(),
);
status = exception.getStatus();
} else {
// For all other exceptions simply return 500 error
body = new BusinessException(
'generic',
`Internal error occurred: ${exception.message}`,
'Internal error occurred',
HttpStatus.INTERNAL_SERVER_ERROR,
);
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// Logs will contain an error identifier as well as
// request path where it has occurred
this.logger.error(
`Got an exception: ${JSON.stringify({
path: request.url,
...body,
})}`,
);
response.status(status).json(body);
}
}
main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// No need to use workaround here as we are using
// ExceptionFilter instead of BaseExceptionFilter
app.useGlobalFilters(new CustomExceptionFilter());
await app.listen(3000);
}
bootstrap();
Let’s test it out!
$ http http://localhost:3000HTTP/1.1 404 Not Found{
"id": "2UUhT7QTJia7OUYp",
"domain": "users",
"message": "User not found",
"timestamp": "2022-07-04T15:18:53.838Z"
}
Check out logs:
[Nest] 20727 - 07/04/2022, 6:18:53 PM ERROR [CustomExceptionFilter] Got an exception: {
"path" : "/",
"id" : "2UUhT7QTJia7OUYp",
"message" : "User not found",
"domain" : "users",
"timestamp" : "2022-07-04T15:18:53.838Z"
}
Awesome!
Summary
In this article, I briefly (actually, not) described how NestJS errors could be enriched and logged in just a few lines of code. Next part describes how to expose your errors as metrics with Prometheus to improve your service observability.
Find the full source code on Github.
Also, you might like my other stories: