- Error handling with EventManager and DefaultError
Error handling with EventManager and DefaultError
This document explains how to use @nestjs-yalc/errors together with @nestjs-yalc/event-manager to:
- log errors with structured payloads,
- emit domain events,
- and return the correct HTTP status code and safe response body to the client.
The recommended entry point is YalcEventService from @nestjs-yalc/event-manager, which wraps the lower-level event* helpers and the DefaultError hierarchy from @nestjs-yalc/errors.
Building blocks
DefaultErrorand subclasses (@nestjs-yalc/errors)- Extend Nest
HttpExceptionand carry:statusCode+statusCodeDescription- a client-safe
messageanderrorname data(internal payload) andmasks(paths to mask)internalMessage(for logs only)eventNameandconfigfor event/monitoring metadata.
- When constructed, they:
- compute a safe
IBetterResponseInterfaceforgetResponse(), - mask
dataas requested, - log immediately if
loggeris enabled, using aLogLevelderived from the HTTP status, - emit an event (
ON_DEFAULT_ERROR_EVENTby default) if anEventEmitter2is provided or globally configured.
- compute a safe
- Specialized classes like
BadRequestError,NotFoundError,TooManyRequestsError, etc. have astatic defaultStatusCodematching the HTTP status they represent; this is what drives both the HTTP code and the default log level.
- Extend Nest
- Event helpers (
@nestjs-yalc/event-manager)event*functions build a unified options object that can:- log via an
ImprovedLoggerService, - emit via
EventEmitter2, - optionally create and throw/return a
DefaultError(or subclass).
- log via an
getLogLevelByStatusandgetLogLevelByErrormap HTTP status codes (or error classes) to a log level:>= 500βerror429βwarn400β499βlog
YalcEventService(@nestjs-yalc/event-manager)- Injectable Nest service that:
- receives an
ImprovedLoggerServiceand anEventEmitter2, - exposes levelled logging methods (
log,warn,debug,verboseand async variants), - exposes error helpers that log, emit and throw a typed
DefaultError(or subclass), - offers
neverthrow-style helpers for functional error handling (*Result,*FromFn).
- receives an
- Injectable Nest service that:
Recommended usage in application code
1. Inject YalcEventService
Wire EventModule in your Nest app and inject YalcEventService where you need logging + error handling:
import { Injectable } from '@nestjs/common';
import { YalcEventService } from '@nestjs-yalc/event-manager';
@Injectable()
export class UserService {
constructor(private readonly events: YalcEventService) {}
}
This gives you a single service that:
- logs through your configured
ImprovedLoggerService, - emits events through the configured
EventEmitter2, - and can raise HTTP-aware errors.
2. Prefer error* helpers over raw HttpException
Instead of throwing Nest exceptions directly, use the HTTP-specific helpers on YalcEventService. Each helper:
- picks the appropriate
DefaultErrorsubclass (and therefore HTTP code), - derives the log level from the status code,
- logs and emits the error event with a structured payload.
Example: semantic 404 with safe client response and internal payload:
async getUserOrFail(id: string) {
const user = await this.repo.findById(id);
if (!user) {
throw this.events.errorNotFound('user.notFound', {
// internal details for logs and events only
data: { userId: id },
// safe payload for the HTTP response
response: { message: 'User not found' },
// optional: mask sensitive fields inside `data`
mask: ['data.userId'],
});
}
return user;
}
Under the hood:
errorNotFoundsetserrorClasstoNotFoundError,NotFoundErrorhasdefaultStatusCode = 404,getLogLevelByStatus(404)resolves to a non-error log level (log),DefaultErrorlogs and emits the event and exposesgetStatus()/getResponse()to Nest.
When this error reaches the HTTP layer:
- the HTTP code is
404, - the response body is taken from the
IBetterResponseInterfaceproduced byDefaultError(status code + description + safe message + any extra fields inresponse), - the internal
dataandinternalMessageare only visible in logs/events, not exposed to the client.
3. Use errorHttp for generic status codes
When you only have an HTTP code (possibly not covered by a dedicated helper), use errorHttp:
throw this.events.errorHttp('user.update.failed', 409, {
data: { userId: id },
response: { message: 'User is in a conflicting state' },
});
This will:
- map
409toConflictErrorviahttpStatusCodeToErrors, - log at a level derived from the status code,
- throw the resulting
DefaultErrorsubclass with the right HTTP code.
4. Use *Result / *FromFn helpers for functional flows
When you prefer not to throw, you can use the neverthrow helpers:
import { Result } from 'neverthrow';
async createUser(dto: CreateUserDto): Promise<Result<User, DefaultError>> {
return this.events.errorFromFn('user.create.failed', async () => {
const existing = await this.repo.findByEmail(dto.email);
if (existing) {
// you can still throw here if you want
throw this.events.errorConflict('user.email.conflict', {
data: { email: dto.email },
response: { message: 'Email already registered' },
});
}
return this.repo.save(dto);
});
}
errorFromFn and friends:
- wrap your callback,
- on success return
ok(value), - on failure forward/convert the error into a
DefaultError, log, emit and return anErr.
5. Keep internal vs external payloads separate
When building error options:
- use
internalMessage(or the first constructor argument ofDefaultErrorand its subclasses) for the full, developer-focused description of the problem; - use
datato attach structured diagnostic data (request ids, input, external error codes, etc.); - use
masks/maskto hide sensitive paths insidedatabefore logging; - use
responseto describe what is safe to send back to the client.
DefaultError will:
- build an
IBetterResponseInterfacebased onresponse, the HTTP code and the error name, - expose it via
getResponse()for the HTTP layer, - include a sanitized snapshot of the payload in logs and events.
How this interacts with Nest exception filters
DefaultError and its subclasses are compatible with Nestβs exception pipeline:
- they extend
HttpException, getStatus()reflects the HTTP status code (from the specialized class or from the wrappedHttpException),getResponse()returns a structured payload (IBetterResponseInterface) that Nest (or your custom filters) can send to the client.
Filters in @nestjs-yalc/errors/filters use the same utilities as YalcEventService to decide log levels (getLogLevelByStatus) and to normalize errors:
- if you throw
DefaultError(or subclasses) viaYalcEventService, logging and event emission usually happen inside the error itself; - filters avoid double-logging these errors and focus on translating non-default errors into HTTP-friendly ones.
In a typical REST setup:
- your application code should use
YalcEventServicefor both logging and error creation, - the resulting errors bubble up through Nestβs filters,
- the client receives a response consistent with the chosen HTTP status and the
responseyou provided, - observability (logs + events) stays consistent because both the
errorsandevent-managerlibraries share the same conventions and helpers.