npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ballistix.digital/exception-mapper

v0.5.0

Published

A NestJS module that maps any thrown value to a coded BaseException and to the ExceptionDto envelope, for HTTP filters and task processors.

Readme

@ballistix.digital/exception-mapper

A NestJS module that maps any thrown value to a coded BaseException and to the ExceptionDto envelope, for HTTP filters and task processors. A mapper turns one kind of thrown value into an exception, and the registry holds the mappers in order. The exception classes, the codes and the envelope live in @ballistix.digital/exception-types.

This file is the consumer guide. The pages that describe how the package is built live in the Ballistix wiki.

Install

npm install @ballistix.digital/exception-mapper @ballistix.digital/exception-types

The package needs Node 22.12 or later. Install these peer dependencies in the application.

| Peer | Range | | --- | --- | | @ballistix.digital/exception-types | the release each published manifest pins | | @nestjs/common | ^11 | | class-validator | ^0.15.1 | | typeorm | >=0.3.0 <2.0.0 | | reflect-metadata | ^0.2.2 |

The application throws the exception classes of the types package, and this package maps to the same classes. A publish of exception-mapper pins the exception-types version of the same build.

Register the module

Register the module once, at the root of the application. It is global, so a service in any feature module injects ExceptionMapperRegistry without an import.

import { Module } from '@nestjs/common';
import { ExceptionModule } from '@ballistix.digital/exception-mapper';

@Module({
	imports: [ExceptionModule.forRoot()],
})
export class AppModule {}

forRootAsync takes the options from a factory, so a ConfigService can decide them:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ExceptionModule } from '@ballistix.digital/exception-mapper';

@Module({
	imports: [
		ExceptionModule.forRootAsync({
			imports: [ConfigModule],
			inject: [ConfigService],
			useFactory: (config: ConfigService) => ({
				exposeErrorDetails: config.get('EXPOSE_ERROR_DETAILS') === 'true',
			}),
		}),
	],
})
export class AppModule {}

The raw driver message of a QueryFailedError can carry SQL text and constraint names. With exposeErrorDetails on, the database mapper adds that message as a second entry of errors, and the title stays the generic message. CAUTION: If an untrusted client reads the response, keep exposeErrorDetails off. The option is off by default.

From a thrown value to the envelope

The registry asks each mapper in one order, and the first match wins.

flowchart TD
    thrown["a thrown value"] --> base{"BaseExceptionMapper<br/>supports?"}
    base -- yes --> same["the same BaseException"]
    base -- no --> validation{"ValidationExceptionMapper<br/>supports?"}
    validation -- yes --> failed["ValidationFailedException<br/>400"]
    validation -- no --> database{"DatabaseExceptionMapper<br/>supports?"}
    database -- yes --> dbError["DatabaseErrorException<br/>400 for SQLSTATE class 22, 23, P0<br/>500 for the rest"]
    database -- no --> generic["GenericExceptionMapper<br/>GenericErrorException<br/>the status of the value, else 500"]
    same --> dtoMapper["ExceptionDtoMapper"]
    failed --> dtoMapper
    dbError --> dtoMapper
    generic --> dtoMapper
    dtoMapper --> envelope["the ExceptionDto envelope"]
    envelope --> retry{"status >= 500?"}
    retry -- yes --> again["a task processor repeats the attempt"]
    retry -- no --> stop["a task processor fails the task"]

Look at the last branch: the status the mappers give is also the retry rule a task processor reads.

The database mapper reads the first two characters of the Postgres SQLSTATE, its class. Class 22 is a data exception, such as a malformed uuid. Class 23 is an integrity constraint violation, such as a unique or foreign key breach. Class P0 is a RAISE EXCEPTION from PL/pgSQL. All three mean the statement itself was wrong, which is the fault of the request, so they give 400. A deadlock or a connection failure gives 500.

ValidationExceptionMapper matches the ValidationException that convertAndValidate throws, and gives one detail for each field and rule that failed. The args of a detail come from the decorator's explicit context when it sets one, else from the rule's own constraint values, read from the class-validator metadata of the validated class. So @Max(24) on months gives { inputField: 'months', rule: 'MAX', args: { max: 24 } } with no change to the DTO.

| Rule | args | | --- | --- | | min, max, minLength, maxLength, arrayMinSize, arrayMaxSize, minDate, maxDate | { min } or { max } | | isLength, isByteLength | { min, max } | | matches | { pattern, modifiers }, the source and flags of the RegExp | | isIn, isNotIn, isEnum | { values } | | isDivisibleBy | { divisor } | | contains, notContains | { seed } | | equals, notEquals | { comparison } | | any other rule with constraint values | { constraints: [...] }, positional | | a rule without constraint values (isString, isNotEmpty) | no args |

The values are JSON-safe: a RegExp becomes its source and flags, a Date its ISO string, and a function or class object is left out. An error without a target keeps the contexts only. GenericExceptionMapper is the fallback the registry uses when no mapper in the list supports the value, so map() never returns undefined.

Write your HTTP filter

The package ships no @Catch() filter. Write one in the application:

import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { ExceptionMapperRegistry } from '@ballistix.digital/exception-mapper';
import { Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
	constructor(private readonly registry: ExceptionMapperRegistry) {}

	public catch(exception: unknown, host: ArgumentsHost): void {
		const response = host.switchToHttp().getResponse<Response>();
		const envelope = this.registry.toExceptionDto(exception);

		response.status(envelope.status).json(envelope);
	}
}

The filter stays in the application, so a breakpoint on the raw thrown value sits in application code. What the filter logs, and what request context it adds, is the decision of the application. See ADR 0002.

Register it as the global filter:

import { APP_FILTER } from '@nestjs/core';

@Module({
	imports: [ExceptionModule.forRoot()],
	providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }],
})
export class AppModule {}

Use it in a task processor

A task processor has no response to write. It stores the envelope on the row, and the status decides whether another attempt can succeed:

@Injectable()
export class ReportTaskProcessor {
	constructor(
		private readonly registry: ExceptionMapperRegistry,
		private readonly repository: Repository<ReportTask>,
	) {}

	public async run(task: ReportTask): Promise<void> {
		try {
			await this.build(task);
		} catch (error) {
			const envelope = this.registry.toExceptionDto(error);

			// A 5xx is a server fault, so the attempt can be repeated. A 4xx is the
			// input's fault and repeats with the same result.
			task.processingError = envelope;
			task.status = envelope.status >= 500 ? TaskStatus.PENDING : TaskStatus.FAILED;

			await this.repository.save(task);
		}
	}
}

The retry rule belongs to the application. This package only says which status the thrown value earns.

Test with the registry

The registry is a plain class. A unit test builds it by hand, with no NestJS bootstrap:

const registry = new ExceptionMapperRegistry(
	new BaseExceptionMapper(),
	new ValidationExceptionMapper(),
	new DatabaseExceptionMapper({ exposeErrorDetails: false }),
	new GenericExceptionMapper(),
	new ExceptionDtoMapper(),
);

expect(registry.toExceptionDto(new Error('boom'))).toMatchObject({
	code: ExceptionCodeEnum.GENERIC_ERROR,
	status: 500,
});

DatabaseExceptionMapper is the only mapper with a constructor argument. Pass {} when the test does not care about the driver message.

API reference

Everything below comes from the package root.

| Export | What it does | | --- | --- | | ExceptionModule.forRoot(options?) | Registers the module with options that are already known | | ExceptionModule.forRootAsync(options) | The same, with imports, inject and a useFactory | | ExceptionModuleOptions | { exposeErrorDetails?: boolean } | | ExceptionModuleAsyncOptions | { imports?, inject?, useFactory } | | EXCEPTION_MODULE_OPTIONS | The injection token of the resolved options | | ExceptionMapperRegistry | Holds the mappers in order. map(error) gives the exception, toExceptionDto(error) gives the envelope | | ExceptionDtoMapper | map(exception) turns one BaseException into the envelope | | ExceptionMapper<TError> | The interface a mapper implements: supports(error) and map(error) | | BaseExceptionMapper | Maps a BaseException to itself | | ValidationExceptionMapper | Maps a ValidationException to ValidationFailedException | | DatabaseExceptionMapper | Maps a TypeORM QueryFailedError to DatabaseErrorException | | GenericExceptionMapper | Maps anything else to GenericErrorException |

Guarantees

  • toExceptionDto() never throws. A detail that fails its own validation travels raw into the envelope instead of replacing the error the caller handles.
  • First match wins. The order of the mappers is a constructor argument, not a search.
  • The generic mapper is the fallback for a value no mapper in the list supports, so map() always returns a BaseException.
  • A validation rule with no ValidationRuleEnum member becomes UNKNOWN, so a new class-validator rule cannot break the mapping.
  • The same thrown value gives the same envelope on every path. An HTTP response and a stored task error read alike.
  • The package ships no filter, no logger and no HTTP dependency.