Skip to content

Capítulo 2.7: Gestión de Errores

Tiempo de estudio: 40 minutos


1. ¿Por qué los errores estándar son malos?

Si ocurre un error en su aplicación Laravel (por ejemplo, no se encuentra un registro en la base de datos) y no lo maneja de ninguna manera, el usuario verá una enorme página HTML con información de depuración o un mensaje poco informativo de "Error del Servidor".

Para una API, esto es una catástrofe. Su aplicación frontend espera recibir JSON, no HTML. Nuestra tarea es interceptar cualquier error y convertirlo en una respuesta JSON estructurada.


2. El despachador central de errores: bootstrap/app.php

En las versiones antiguas de Laravel, había un archivo voluminoso App\Exceptions\Handler.php. En Laravel 11/12, todo es mucho más simple y elegante. El centro de gestión de errores ahora se encuentra directamente en el archivo de configuración de su aplicación: bootstrap/app.php.

Abra bootstrap/app.php. Al final, verá el bloque .withExceptions(...). Este es nuestro "despachador central".

<?php
// bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // ...
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // <-- AQUÍ ES DONDE TRABAJAREMOS
    })->create();

3. Manejando el error más común: "No encontrado" (404)

El error más común en una API es cuando el usuario solicita un recurso que no existe (por ejemplo, GET /api/planets/999). Laravel en este caso genera una excepción ModelNotFoundException o NotFoundHttpException. Vamos a interceptarlas.

Agregue el siguiente código dentro de .withExceptions(...):

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // Interceptamos la excepción cuando el modelo no se encuentra en la base de datos
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        // Verificamos que la solicitud provenga específicamente de nuestra API
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'El recurso solicitado no se encuentra en nuestra galaxia.'
            ], 404);
        }
    });

    // Interceptamos la excepción cuando la ruta misma no se encuentra
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Esta ruta cósmica no existe.'
            ], 404);
        }
    });

})->create();
¿Qué hicimos?

  1. $exceptions->render(...) — Registramos un "manejador". Dice: "Si ocurre una excepción de tipo ModelNotFoundException, ejecuta este código".
  2. if ($request->is('api/*')) — Esta es una verificación importante. Garantiza que nuestra bonita respuesta JSON solo se enviará para solicitudes de API, sin afectar las páginas web normales.
  3. return response()->json(...) — Creamos y devolvemos una respuesta JSON estandarizada con el código 404.

Ahora, si solicita un planeta inexistente, en lugar de una página HTML fea, recibirá un JSON ordenado.


4. Excepciones personalizadas: Creando nuestras propias "señales de alarma"

A veces, las excepciones estándar no son suficientes. Imaginemos que tenemos una regla de negocio: "no se puede eliminar el planeta 'Tierra'". Si alguien intenta hacerlo, debemos devolver un error significativo.

Paso 1: Creamos nuestra propia clase de excepción Ejecutamos en la terminal:

php artisan make:exception CannotDeleteEarthException

Paso 2: La usamos en el controlador Abrimos PlanetController.php y modificamos el método destroy:

<?php
// app/Http/Controllers/PlanetController.php
use App\Exceptions\CannotDeleteEarthException; // <-- Importamos nuestra excepción
use App\Models\Planet;

public function destroy(Planet $planet)
{
    // Nuestra nueva regla de negocio
    if (strtolower($planet->name) === 'земля') {
        throw new CannotDeleteEarthException('La eliminación del planeta Tierra está prohibida por el Código Galáctico.');
    }

    $planet->delete();
    return response()->json(null, 204);
}
Ahora, si alguien intenta ejecutar DELETE /api/planets/1 (donde 1 es el ID de la Tierra), nuestro código lanzará una excepción CannotDeleteEarthException.

Paso 3: Enseñamos a Laravel a manejar bellamente nuestra "alarma" Volvamos a bootstrap/app.php y agreguemos un nuevo manejador para nuestra excepción.

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // Nuestro nuevo manejador
    $exceptions->render(function (CannotDeleteEarthException $e, Request $request) {
        return response()->json([
            'message' => 'Operación prohibida.',
            'details' => $e->getMessage() // Obtenemos el mensaje que pasamos en throw
        ], 403); // 403 Forbidden - "Acceso denegado"
    });

    // ... (otros manejadores para 404)

})->create();
¡Listo! Creamos nuestra propia excepción con nombre, que hace que el código del controlador sea más limpio, y enseñamos a Laravel a convertirla en una respuesta JSON hermosa y significativa con el estado HTTP correcto.


5. Gestión de todos los demás fallos (500 Internal Server Error)

¿Qué hacer con todos los demás errores inesperados? Por ejemplo, si la base de datos se cae o hay un error de sintaxis en el código. Para esto, podemos registrar un manejador "universal" para el tipo de error más general: Throwable.

Importante: Este manejador debe ser el último para no interceptar excepciones más específicas que definimos anteriormente.

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // ... (manejadores para CannotDeleteEarthException y 404)

    // MANEJADOR UNIVERSAL (al final de todo)
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->is('api/*')) {
            // En modo de depuración, se puede mostrar el mensaje de error real
            $message = config('app.debug')
                ? 'Ha ocurrido un error: ' . $e->getMessage()
                : 'Ha ocurrido un error inesperado a bordo. Los ingenieros ya han sido llamados.';

            return response()->json(['message' => $message], 500);
        }
    });

})->create();

Ahora, cualquier excepción "desconocida" será interceptada cuidadosamente y convertida en JSON con el código 500, sin romper su API ni mostrar información innecesaria al usuario.


6. Registro de errores: La caja negra de la nave espacial

Configuración de registro en config/logging.php:

<?php
'channels' => [
    'space_api' => [
        'driver' => 'daily',
        'path' => storage_path('logs/space_api.log'),
        'level' => 'error',
        'days' => 14,
    ],
],

Añadir un registro al log:

<?php
try {
    // Código con riesgo de error
} catch (Exception $e) {
    Log::channel('space_api')->error('Error de acceso a los planetas', [
        'exception' => $e,
        'request' => request()->all(),
        'user_id' => auth()->id()
    ]);
    throw $e;
}


Quiz de afianzamiento

1. Estado HTTP para "Planeta no encontrado":

2. Clase para el manejo global de errores:

3. Método para crear una excepción personalizada:

4. Canal para el registro separado de errores de API:

5. Principal ventaja de crear excepciones personalizadas:


🚀 Resumen del capítulo:

Has equipado tu API con un robusto sistema de rescate:

  • 🛟 Intercepción global de errores estándar
  • 🪐 Excepciones personalizadas con códigos comprensibles
  • 📝 Formato JSON unificado para todos los errores
  • 🔍 Registro con detalles del incidente
  • 📡 Integración con sistemas de monitoreo

¡La nave espacial está lista para emergencias! En el capítulo final de la sección, probaremos todos los sistemas.

📌 Verificación:

  1. Crea la excepción PlanetNotFoundException
  2. Agrega el manejo de errores 404 en ->withExceptions
  3. Prueba la solicitud a un planeta inexistente

⚠️ Si los errores no se interceptan:

  • Asegúrate de que is('api/*') coincide con tus rutas
  • Verifica el orden de los manejadores en register()
  • Para excepciones personalizadas, usa throw new