NestJS

Nest

Tutorial Nest: Gestión de errores en controladores

Aprende a manejar errores en controladores NestJS con excepciones, filtros, interceptores y pipes para APIs robustas y seguras.

Aprende Nest y certifícate

La gestión de errores es una parte esencial de cualquier aplicación, ya que permite manejar situaciones inesperadas de manera adecuada, presentando mensajes claros al usuario y evitando que la aplicación falle de manera catastrófica.

NestJS proporciona un enfoque modular y extensible para manejar errores en sus controladores y en otras partes de una aplicación.

Excepciones básicas en NestJS

NestJS viene con un conjunto de excepciones predefinidas que puedes lanzar desde tus controladores o servicios. Estas excepciones se traducen automáticamente en respuestas HTTP con el código de estado adecuado, simplificando el manejo de errores comunes en APIs RESTful.

A continuación, exploraremos las más comunes:

NotFoundException

La excepción NotFoundException se utiliza cuando un recurso solicitado no se encuentra en la aplicación.

NotFoundException es útil para manejar situaciones en las que se espera un recurso específico (como un registro en una base de datos) pero no está presente.

import { Controller, Get, NotFoundException } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get(':id')
  findOne(@Param('id') id: string): string { // Agregamos @Param para extraer el ID
    // Simulación: el ítem no se encuentra
    const item = null; // O una búsqueda que no devuelve resultados

    if (!item) {
      throw new NotFoundException(`El elemento con el ID "${id}" no se encontró.`);
    }
    return item;
  }
}

Si el elemento no se encuentra, se lanza una excepción NotFoundException. Esto resultará en una respuesta HTTP con el código de estado 404 Not Found y un mensaje que indica "El elemento con el ID ${id} no se encontró".

UnauthorizedException

La excepción UnauthorizedException se lanza cuando un usuario o solicitud no está autorizado para acceder a un recurso o realizar una acción específica.

Es esencial para el control de acceso y la seguridad.

Ejemplo:

import { UnauthorizedException, Controller, Get } from '@nestjs/common';

@Controller('recursos')
export class RecursosController {
  @Get()
  obtenerRecursos() {
    // Verificar si el usuario tiene autorización
    if (!usuarioTieneAutorizacion()) {
      throw new UnauthorizedException('No está autorizado para acceder a este recurso');
    }
    // Procesar la solicitud si está autorizado
    return 'Recursos obtenidos con éxito';
  }
}

En este ejemplo, si la función usuarioTieneAutorizacion() devuelve false, se lanza una UnauthorizedException, lo que resulta en una respuesta HTTP con el código de estado 401 Unauthorized y el mensaje "No está autorizado para acceder a este recurso".

BadRequestException

La excepción BadRequestException se utiliza cuando una solicitud no es válida debido a datos incorrectos o incompletos enviados por el cliente.

BadRequestException es útil para manejar situaciones en las que se espera una solicitud con datos específicos, pero los datos no son válidos.

Ejemplo:

import { BadRequestException, Controller, Post, Body } from '@nestjs/common';

@Controller('recursos')
export class RecursosController {
  @Post()
  crearRecurso(@Body() datos: any) {
    // Simulación: validar los datos recibidos
    if (!datos || !datos.nombre || typeof datos.nombre !== 'string') {
      throw new BadRequestException('Los datos proporcionados para el nombre son incorrectos o incompletos.');
    }
    return 'Recurso creado con éxito';
  }
}

En este ejemplo, si los datos recibidos no cumplen con ciertas reglas de validación (en este caso, la presencia de un campo nombre), se lanza una BadRequestException. Esto resultará en una respuesta HTTP con el código de estado 400 Bad Request y el mensaje "Los datos proporcionados para el nombre son incorrectos o incompletos".

InternalServerErrorException

La excepción InternalServerErrorException se utiliza cuando ocurre un error inesperado en el servidor.

Generalmente, InternalServerErrorException se lanza en situaciones inesperadas o problemas en la lógica interna del servidor.

Ejemplo:

import { InternalServerErrorException, Controller, Get } from '@nestjs/common';

@Controller('recursos')
export class RecursosController {
  @Get()
  obtenerRecursos() {
    try {
      // Lógica para obtener recursos
    } catch (error) {
      throw new InternalServerErrorException('Se produjo un error interno al obtener los recursos');
    }
  }
}

En este ejemplo, si se produce un error interno durante la obtención de los recursos se lanza una InternalServerErrorException. Esto resultará en una respuesta HTTP con el código de estado 500 Internal Server Error y un mensaje que indica "Se produjo un error interno al obtener los recursos".

ForbiddenException

La excepción ForbiddenException se utiliza cuando un usuario o solicitud no está autorizado para acceder a un recurso o realizar una acción específica.

Es útil para el control de acceso y la seguridad.

ForbiddenException es similar a UnauthorizedException, pero se diferencia en que el usuario puede estar autenticado pero no autorizado.

Ejemplo:

import { ForbiddenException, Controller, Put, Param } from '@nestjs/common';

@Controller('recursos')
export class RecursosController {
  @Put(':id')
  actualizarRecurso(@Param('id') id: string) {
    if (!usuarioTienePermisosDeEdicion(id)) {
      throw new ForbiddenException('No tiene permisos para actualizar este recurso');
    }
    // Procesar la actualización si está autorizado
  }
}

En este ejemplo, si la función usuarioTienePermisosDeEdicion(id) devuelve false, se lanza una ForbiddenException. Esto resultará en una respuesta HTTP con el código de estado 403 Forbidden y un mensaje que indica "No tiene permisos para actualizar este recurso".

ConflictException

La excepción ConflictException se utiliza cuando se detecta un conflicto, como intentar crear un recurso que ya existe.

Esta excepción es útil en situaciones donde se requiere una acción especial para resolver el conflicto.

Ejemplo:

import { ConflictException, Controller, Post, Body } from '@nestjs/common';

@Controller('recursos')
export class RecursosController {
  @Post()
  crearRecurso(@Body() datos: any) {
    if (recursoYaExiste(datos.nombre)) {
      throw new ConflictException('El recurso con este nombre ya existe');
    }
    // Procesar la creación del recurso si no existe conflicto
  }
}

En este ejemplo, si la función recursoYaExiste(datos.nombre) devuelve true, se lanza una ConflictException. Esto resultará en una respuesta HTTP con el código de estado 409 Conflict y un mensaje que indica "El recurso con este nombre ya existe".

NotImplementedException

La excepción NotImplementedException se utiliza cuando una funcionalidad no ha sido implementada todavía. Es útil para indicar que una funcionalidad está en desarrollo o que se planea implementar en el futuro.

Ejemplo:

import { NotImplementedException, Controller, Get } from '@nestjs/common';

@Controller('recursos')
export class RecursosController {
  @Get()
  obtenerRecursos() {
    throw new NotImplementedException('Esta función aún no ha sido implementada');
  }
}

En este ejemplo, si se llama a la ruta /recursos para obtener recursos, se lanzará una NotImplementedException con el mensaje "Esta función aún no ha sido implementada".

Sin embargo, esta excepción no generará una respuesta HTTP específica, ya que no se trata de un error relacionado con el protocolo HTTP, sino más bien con la lógica interna de la aplicación.

Creación de excepciones personalizadas

Si las excepciones básicas no satisfacen las necesidades específicas, es posible crear excepciones personalizadas extendiendo la clase base HttpException.

Ejemplo:

// src/common/exceptions/mi-excepcion-personalizada.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class MiExcepcionPersonalizada extends HttpException {
  constructor(mensaje: string = 'Ocurrió un error personalizado', estado: HttpStatus = HttpStatus.BAD_REQUEST) {
    super(mensaje, estado);
  }
}

Luego, esta excepción personalizada se puede lanzar de la misma manera que las excepciones incorporadas:

// src/items/items.controller.ts
import { Controller, Get } from '@nestjs/common';
import { MiExcepcionPersonalizada } from '../common/exceptions/mi-excepcion-personalizada.exception'; // Ruta a tu excepción

@Controller('items')
export class ItemsController {
  @Get('/personalizado')
  findCustomError() {
    // Lógica que decide lanzar tu excepción personalizada
    const condicionDeError = true;
    if (condicionDeError) {
      throw new MiExcepcionPersonalizada('Hubo un problema específico en tu solicitud.', HttpStatus.FORBIDDEN);
    }
    return 'Operación exitosa.';
  }
}

Filtros de excepción

Los filtros de excepción en NestJS permiten interceptar las excepciones lanzadas (ya sean incorporadas o personalizadas) y transformarlas en respuestas HTTP personalizadas. Esto es ideal para formatear las respuestas de error de forma consistente en toda tu aplicación.

Para crear un filtro de excepción, primero es necesario implementar la interfaz ExceptionFilter.

Ejemplo:

// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException) // Este filtro capturará cualquier HttpException
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const errorResponse = exception.getResponse(); // Obtiene el objeto de respuesta original de la excepción

    // Puedes personalizar el mensaje de error si es un string o un objeto
    const message = (typeof errorResponse === 'object' && 'message' in errorResponse)
      ? errorResponse.message
      : exception.message;

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: message,
        // Puedes añadir más campos aquí, como un código de error interno
      });
  }
}

Para asociar este filtro a un controlador específico, se utiliza el decorador @UseFilters():

// src/items/items.controller.ts
import { Controller, Get, UseFilters, NotFoundException } from '@nestjs/common';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter'; // Ruta a tu filtro

@Controller('items')
@UseFilters(HttpExceptionFilter) // Aplica el filtro a todo el controlador
export class ItemsController {
  @Get('/error-filtrado')
  triggerFilteredError() {
    throw new NotFoundException('Este error será capturado por el filtro personalizado.');
  }
}

Filtros de excepción globales

Si deseas que un filtro de excepción se aplique a todos los controladores y rutas de tu aplicación, puedes configurarlo como un filtro global en tu archivo main.ts:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter'; // Ruta a tu filtro

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // Aplica el filtro globalmente para toda la aplicación
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

Interceptores para transformación de errores

Mientras que los filtros de excepción se centran en el manejo de errores, los interceptores en NestJS pueden ser utilizados para transformar respuestas o errores de una manera más general.

Los interceptores pueden ser útiles, por ejemplo, si se quiere envolver todas las respuestas en un objeto específico o transformar errores antes de que lleguen a un filtro de excepción.

Un interceptor puede interceptar la respuesta de un método de controlador, y tiene la capacidad de transformarla antes de que se envíe al cliente.

Ejemplo de un interceptor que envuelve todas las respuestas en un objeto específico:

// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(map(data => ({ data })));
  }
}

Para usar este interceptor en un controlador, se puede aplicar el decorador @UseInterceptors():

// src/items/items.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from '../common/interceptors/transform.interceptor';

@Controller('items')
@UseInterceptors(TransformInterceptor) // Envuelve las respuestas de este controlador
export class ItemsController {
  @Get()
  findAll() {
    return ['item1', 'item2'];
  }
}

La respuesta sería:

{
  "data": ["item1", "item2"]
}

Los interceptores también pueden capturar errores usando el operador catchError de RxJS dentro del pipe() para transformarlos antes de que lleguen a los filtros de excepción.

Pipes para validación y transformación de errores

Los pipes en NestJS se pueden usar para transformar y validar los datos de entrada. Si la validación falla, un pipe puede lanzar una excepción.

Un ejemplo común, que ya hemos visto en lecciones anteriores de POST y PUT, es el ValidationPipe. Este pipe, cuando está configurado globalmente (como recomendamos en tu main.ts), utiliza class-validator para validar automáticamente los DTOs. Si la validación falla, el ValidationPipe lanza una BadRequestException.

Por ejemplo, utilizando class-validator y class-transformer, es posible definir reglas de validación y luego usar el ValidationPipe para aplicar esas reglas:

// src/items/dto/create-item.dto.ts
import { IsNotEmpty, IsInt } from 'class-validator';

export class CreateItemDto {
  @IsNotEmpty()
  name: string;

  @IsInt()
  price: number;
}

En el controlador, si ya tienes el ValidationPipe configurado globalmente, simplemente usas el DTO:

// src/items/items.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateItemDto } from './dto/create-item.dto'; // Tu DTO con validadores

@Controller('items')
export class ItemsController {
  @Post()
  create(@Body() createItemDto: CreateItemDto) { // El ValidationPipe global actúa aquí
    // Si la validación falla (ej. 'name' vacío, 'price' no es número),
    // el ValidationPipe global lanzará automáticamente un BadRequestException.
    // De lo contrario, el código de creación del ítem se ejecutará normalmente.
    return `Ítem "${createItemDto.name}" creado con precio ${createItemDto.price}.`;
  }
}

Este BadRequestException lanzado por el ValidationPipe puede ser capturado por un filtro de excepción global (como el HttpExceptionFilter que creamos antes) para estandarizar la respuesta de error.

Flujo de Ejecución y Gestión de Errores en NestJS

Comprender el orden en que NestJS procesa las solicitudes y maneja los errores a través de sus diferentes capas es crucial:

  1. Incoming Request: La solicitud HTTP llega a la aplicación NestJS.
  2. Middlewares: Si hay middlewares configurados (ej. helmet, cors), se ejecutan primero.
  3. Guards: Si hay guards en la ruta, se ejecutan para verificar la autenticación/autorización. Si un guard lanza una excepción, esta es capturada.
  4. Interceptors (pre-controller): Los interceptores globales o de controlador/método se ejecutan antes que el controlador. Pueden modificar la solicitud o el contexto.
  5. Pipes: Los pipes (ej. ValidationPipe, ParseIntPipe) se ejecutan para transformar y/o validar los datos de entrada de los parámetros del controlador (@Body(), @Param(), @Query()). Si un pipe lanza una excepción (como un BadRequestException por validación fallida), esta es capturada.
  6. Controller Method: El método del controlador se ejecuta. Aquí es donde tu lógica de negocio principal y el lanzamiento de excepciones (como NotFoundException, ConflictException, etc.) suelen ocurrir.
  7. Interceptors (post-controller/error): Una vez que el método del controlador devuelve un valor o lanza una excepción, los interceptores pueden interceptar la respuesta exitosa (usando map como en el ejemplo) o un error (usando catchError en RxJS).
  8. Exception Filters: Finalmente, si una excepción (lanzada por un guard, pipe, el controlador, o un interceptor) no ha sido manejada por un interceptor, los filtros de excepción (a nivel de método, controlador o global) la capturarán para transformarla en la respuesta HTTP final.
  9. Outgoing Response: La respuesta HTTP formateada (ya sea exitosa o con un error) se envía de vuelta al cliente.

En resumen: los pipes y los interceptores pueden lanzar o transformar errores antes de que la lógica principal del controlador se complete o la respuesta se envíe, mientras que los filtros de excepción son la última línea de defensa para formatear cualquier excepción no capturada de forma personalizada.

Conclusión

NestJS proporciona una arquitectura sofisticada y en capas para la gestión de errores. Desde las excepciones incorporadas para escenarios comunes, pasando por la creación de excepciones personalizadas, hasta mecanismos avanzados como filtros de excepción, interceptores y pipes, tienes todas las herramientas necesarias para construir una gestión de errores robusta, personalizada y consistente que se adapte a las necesidades específicas de tu proyecto. Entender el flujo de ejecución de NestJS te permitirá decidir qué herramienta usar en cada etapa para manejar tus errores de la manera más efectiva.

Aprende Nest online

Otras lecciones de Nest

Accede a todas las lecciones de Nest y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Nest y certifícate

Ejercicios de programación de Nest

Evalúa tus conocimientos de esta lección Gestión de errores en controladores con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.