How do I organise throwing business-logic exceptions in NestJs services?

This is a nice question I've been dealing with lately.

For some project we wanted to have business errors shared across different domains of our application. What we ended up with was to simply have a shared (or common) folder or module exposing the different domain specific errors we have in our app.

On top on that we use the common exceptions from Nest which are just fine for this job !

Finally, we customize common Nest exceptions with our shared domain specific business errors.

Minimalist user.service.ts reproduction example:

import { NotFoundException, Logger } from '@nestjs/common';
import { UserBusinessErrors } from '../../shared/errors/user/user.business-errors';

// some code ...

// Find the users from the usersIds array
const dbUsers = await this.userRepository.find({ id: In(usersIds) });
// If none was found, raise an error
if (!dbUsers) {
    Logger.error(`Could not find user with IDs: ${usersIds}`, '', 'UserService', true);
    throw new NotFoundException(UserBusinessErrors.NotFound);
}

with its corresponding user.business-errors.ts file within a shared or common module/folder (e.g src/shared/errors/user/user.business-errors.ts):

export const UserBusinessErrors = {
    // ... other errors

    NotFound: {
        apiErrorCode: 'E_0002_0002',
        errorMessage: 'User not found',
        reason: `Provided user ids doesn't exist in DB`
    },

    // ... other errors
}

Or sometimes if you want something more generic, you could use shared errors too:

import { InternalServerErrorException, Logger } from '@nestjs/common';
import { SharedBusinessErrors } from '../../shared/errors/shared.business-errors';

// some code ...

try {
    // ...
} catch (error) {
    Logger.log(SharedBusinessErrors.DbSaveError, 'UserService');
    throw new InternalServerErrorException(SharedBusinessErrors.DbSaveError, error.stack);
}

with corresponding shared.business-errors.ts file:

export const SharedBusinessErrors = {
    DbSaveError: {
        apiErrorCode: 'E_0001_0001',
        errorMessage: `Error when trying to save resource into the DB`,
        reason: 'Internal server error'
    },

    // ... other errors
}

Last thoughts to comment on your different questions:

  • As you stated

Is creating global- or module-level filters a way to go? Decorating every endpoint with UseFilters seems pretty cumbersome.

  • We could take advantage of Nest filters customization and use decorators at the controller or route level too. But to me, you would actually still need to specify what business errors it is related to, so that you add value to you business error and you'd need to raise the error in your service anyway. So having just some shared files containing the domain specific (or business) errors and the more generic ones is fine imo (but maybe not the cleanest / most elegant solution tho).

  • You could put your business errors files within each module's folder and add a webpack config to gather and concatenate all of them into a unique file.

Finally I guess you could even create a npm package if you need those domain specific or shared errors across multiple projects.

Let me know if I'm unclear on anything or/and if it gives you a bit more insight !


Ok let's focus on keeping the exception in the shared folder from there we got the approach you say from creating a global exception filter for HttpExceptions it is not a bad approach and you can format the output in the exception itself. Other way could be to create a BusinessDomainFilter and catch only the exceptions that are domain related:

@Catch(EntityNotFoundException, ActionForbiddenException)
@Injectable()
export class BusinessDomainFilter implements ExceptionFilter {
...

and map there only those exceptions leaving the rest to the application filter