Skip to content

Errors — NestJS exceptions, not `throw new Error`

Map domain failures to NestJS HTTP exceptions — clients and logs get consistent, classifiable errors.

nestjs-data-nestjs-exceptions

Why it matters

Failure modes if this rule is ignored
StakeIf ignored
Unclear failure
  • Generic 500 in two different cases = inability to distinguish a bug from bad input.
  • Ad-hoc error shapes per controller — clients and logs cannot classify failures consistently.

How to fix

Throwing BadRequestException, NotFoundException, ForbiddenException lets NestJS map to the right HTTP status. throw new Error() collapses to a generic 500 and hides real failures.

Even better: domain exceptions mapped by a NestJS exception filter to a unified error format. The service doesn't know about HTTP — it throws domain errors. The filter maps.

Examples

Bad
ts
async getSite(id: string) {
  const site = await this.repo.findOne({ where: { id } });
  if (!site) throw new Error('Site not found');     // ← 500
  if (!site.isPublic) throw new Error('Forbidden'); // ← also 500. Same response for two different failures.
  return site;
}
Good
ts
// Option A — built-in NestJS exceptions at the service boundary
async getSite(id: string, requester: User) {
  const site = await this.repo.findOne({ where: { id } });
  if (!site) throw new NotFoundException(`Site ${id} not found`);
  if (!site.isPublic && site.ownerId !== requester.id) {
    throw new ForbiddenException('Cannot access this site');
  }
  return site;
}

// Option B — domain exceptions + a filter (preferred for larger apps)
// libs/catalog/backend-feature/src/lib/errors.ts
export class SiteNotFoundError extends Error {
  constructor(public siteId: string) { super(`Site ${siteId} not found`); }
}
export class SiteForbiddenError extends Error {
  constructor(public siteId: string) { super(`Cannot access site ${siteId}`); }
}

// libs/shared/backend/src/lib/domain-exception.filter.ts
@Catch()
export class DomainExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse();
    if (exception instanceof SiteNotFoundError) {
      return res.status(404).json({ code: 'site_not_found', siteId: exception.siteId });
    }
    if (exception instanceof SiteForbiddenError) {
      return res.status(403).json({ code: 'site_forbidden', siteId: exception.siteId });
    }
    throw exception;
  }
}

// service stays HTTP-agnostic
async getSite(id: string, requester: User) {
  const site = await this.repo.findOne({ where: { id } });
  if (!site) throw new SiteNotFoundError(id);
  if (!site.isPublic && site.ownerId !== requester.id) {
    throw new SiteForbiddenError(id);
  }
  return site;
}

Contribute

Released under the MIT License.

esc