ecosystem-wikicómo extender los templates sin romper sus convenciones
‹ todas las recipes

Errores de dominio

template-api
último cambio 2026-09-25

En petid-api hay 35 errores de dominio, uno por fichero, repartidos por domain/<feature>/errors/. Parece mucho ceremonial para nueve líneas cada uno, pero es lo que permite que añadir un error nuevo no obligue a tocar nada más.

La forma

Todos extienden la misma base, que está en domain/errors/domain.error.ts y es esto entero:

export abstract class DomainError extends Error {
  abstract readonly statusCode: number;
  abstract readonly code: string;
}

Y un error concreto:

export class PetTagRevokedError extends DomainError {
  readonly statusCode = 410;
  readonly code = "PET_TAG_REVOKED";
  constructor() {
    // 410 Gone y no 404: la chapa existió y la fila sigue en la BD por
    // trazabilidad. Lo que ya no se sirve son los datos del dueño.
    super("This tag has been replaced and is no longer active");
  }
}

Tres piezas:

Quién los traduce

Un único filtro, registrado globalmente. No hay try/catch en los controllers:

@Catch(DomainError)
export class DomainErrorFilter implements ExceptionFilter {
  catch(ex: DomainError, host: ArgumentsHost): void {
    const res = host.switchToHttp().getResponse<Response>();
    res.status(ex.statusCode).json({
      statusCode: ex.statusCode,
      code: ex.code,
      message: ex.message,
    });
  }
}

Al lado vive AllExceptionsFilter, que recoge lo inesperado. La diferencia de intención: si es un DomainError, es una situación prevista del negocio; cualquier otra cosa es un fallo.

Dónde se lanzan

En el dominio y en la aplicación, nunca en la presentación:

const pet = await this.pets.findById(petId);
if (!pet) throw new PetNotFoundError(petId);   // en el use case o el service
pet.assertAccessibleBy(requester);             // o dentro de la propia entidad

Si un controller está lanzando un error de negocio, esa comprobación está en la capa equivocada.

Al añadir uno