DEV Community

Micael Levi L. C.
Micael Levi L. C.

Posted on • Updated on

NestJS tip: fine-grained exception filtering for the same exception class!

for NestJS v8, v9 and v10

What

We can now have multiple exception filters for the same error class like this:

class MongoDbDuplicatedKeyConstraintErrorToCatch {
  static [Symbol.hasInstance](instance: unknown): boolean {
    // Any condition here:
    return instance instanceof MongoError && instance.code === 11000
  }
}

// This only catches `MongoError` errors that has the error code '11000'
@Catch(MongoDbDuplicatedKeyConstraintErrorToCatch)
export class MongoDbDuplicatedKeyConstraintFilter implements ExceptionFilter {
  catch(exception: MongoError, host: ArgumentsHost) {
    // Your exception handler code here ...
  }
}
Enter fullscreen mode Exit fullscreen mode

or using the nestjs-conditional-exception-filter package to make it generic:

import { Catch, ExceptionFilter } from '@nestjs/common'
import { filter } from 'nestjs-conditional-exception-filter'

@Catch(
  filter({
    for: MongoError,
    when: error => error.code === 11000,
  })
)
export class MongoDbDuplicatedKeyConstraintFilter implements ExceptionFilter {
  catch(exception: MongoError, host: ArgumentsHost) {
    // Your exception handler code here ...
  }
}
Enter fullscreen mode Exit fullscreen mode

I don't know about you but I'd rather use the above approach than the following one:

alternative

Why

Let's say you want to handle exceptions raised by some 3rd-party package but such exceptions are somewhat too generic, they use the same error class to represent different errors.

A real world example would be the QueryFailedError from typeorm, or MongoError from mongodb package. Both of them are wrappers. The error object may have some discriminated field that holds the underlying platform error code.

We could use exception filters from @nestjs/common to handle them but such abstraction doesn't allows us to conditionally catch the exception. This missing feature was requested in 2020 here: https://github.com/nestjs/nest/issues/4516 and in 2023 here: https://discord.com/channels/520622812742811698/1085108578257489970/1085108578257489970 (and few other times)

Thus if we want to handle some specific error raised by mongodb lib, we would have to catch all objects that are instance of MongoError to later conditionally treat it, like this:

@Catch(MongoError)
export class MongoErrorFilter extends BaseExceptionFilter {
  catch(exception: MongoError, host: ArgumentsHost) {
    if (exception.code === 11000) {
      // Your exception handling code here ...
    } else {
      return super.catch(exception, host)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Imagine how fast that code would grow when you starting handling others error codes for the same MongoError exception. And how confusing it will look like since it violates the Single Responsibility Principle and the Open-Closed Principle.

Of course that you could split the code into several files and use the Strategy design pattern but you could also follow that other approach I just showed you here. Keep reading to understand how that works.

How

Under the hood, NestJS will look up for the exception filter that, for a given exception raised from your app, matches the following condition: exception instanceof ExceptionMetaType (here's the code)

Knowing that, we can use the well-known JavaScript symbol Symbol.hasInstance to create a class that will act like a 'attribute-based exception filter' that Nest will invoked later:

class FooError {
  static [Symbol.hasInstance](instance: any) {
    // Any condition here:
    return typeof instance.bar !== 'undefined'
  }
}
Enter fullscreen mode Exit fullscreen mode

to later use that as the error being caught by our exception filter like so:

import { Catch, ExceptionFilter } from '@nestjs/common'

@Catch(FooError)
export class MyFooErrorFilter implements ExceptionFilter {
  catch(exception, host) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

now, for that FooError, any error raised with a field bar in it will be caught by our exception filter. If the error does not meet this condition, another filter could catch it, as usual.

Top comments (0)