Saltar al contenido principal

Visión general

Al crear un proyecto Laravel, el manejo de errores y excepciones ya viene configurado. Personalízalo con el método withExceptions en bootstrap/app.php.

Flujo de manejo de excepciones

Cómo transcurre el flujo desde que se lanza una excepción hasta que se devuelve una respuesta al cliente.
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;

return Application::configure(basePath: dirname(__DIR__))
    ->withExceptions(function (Exceptions $exceptions): void {
        // Configura aquí el reporte y renderizado de excepciones
    })->create();
El objeto $exceptions que recibe la closure de withExceptions es una instancia de Illuminate\Foundation\Configuration\Exceptions que gestiona el manejo de excepciones de toda la aplicación.

Configuración de debug

La opción debug de config/app.php controla el nivel de detalle mostrado. Por defecto usa el valor de APP_DEBUG en .env.
# Desarrollo local
APP_DEBUG=true

# Producción
APP_DEBUG=false
En producción, deja siempre APP_DEBUG=false. Si se queda en true, hay riesgo de exponer información sensible al usuario final.

Reporte de excepciones

Reportar una excepción es registrarla en el log o enviarla a servicios externos como Sentry o Flare. Por defecto se registra en el log según la configuración de config/logging.php.

Callback personalizado de reporte

Para reportar según el tipo de excepción, pasa una closure a report. Laravel detecta el tipo por el type-hint de la closure.
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // Notificar a un servicio externo, etc.
    });
})
Aun registrando un callback personalizado, el logging por defecto continúa. Para cortar la propagación, llama a stop() o devuelve false.
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();
})

Helper report()

Para reportar sin mostrar una página de error, usa el helper report().
public function isValid(string $value): bool
{
    try {
        // Lógica de validación...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}
El helper report() te permite dejar constancia del error sin interrumpir la respuesta al usuario. Útil para excepciones no críticas o de jobs en segundo plano.

Evitar reportes duplicados

Si la misma instancia de excepción se pasa varias veces a report(), pueden generarse entradas duplicadas. Con dontReportDuplicates(), la misma instancia se registra solo la primera vez.
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportDuplicates();
})
$original = new RuntimeException('Whoops!');

report($original); // Se registra

try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); // Se ignora (misma instancia)
}

Contexto global del log

Para añadir información común a todos los logs de excepción, usa context. Si está disponible, el ID del usuario actual se añade automáticamente.
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->context(fn () => [
        'app_version' => config('app.version'),
    ]);
})

Método context() en la clase de excepción

Definir un método context() en la propia clase de excepción permite añadir información específica de esa excepción al log.
<?php

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    public function __construct(
        private readonly int $orderId,
        string $message = '',
    ) {
        parent::__construct($message);
    }

    /**
     * Devuelve el contexto de la excepción
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

Cambiar el nivel de log

Para registrar ciertas excepciones con un nivel específico, usa level.
use PDOException;
use Psr\Log\LogLevel;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->level(PDOException::class, LogLevel::CRITICAL);
})

Limitación de reportes

Si se generan muchas excepciones, throttle te permite controlar cuántas se reportan.
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    // Reporta aleatoriamente 1 de cada 1000
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})
Para limitar por minuto, usa Limit.
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300);
        }
    });
})

Renderizado de excepciones

Renderizar consiste en convertir la excepción en una respuesta HTTP. Por defecto Laravel genera la respuesta adecuada; también puedes personalizar el resultado.

Callback personalizado de renderizado

Pasa una closure a render para convertir la excepción en una respuesta.
use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})
Puedes sobrescribir el renderizado de excepciones integradas (NotFoundHttpException, etc.). Si la closure no devuelve un valor, se usa el renderizado por defecto.
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.',
            ], 404);
        }
        // Devolver null muestra la página 404 por defecto
    });
})

Selección automática JSON / HTML

Laravel decide si devolver HTML o JSON según la cabecera Accept. Para personalizarlo, usa shouldRenderJsonWhen.
use Illuminate\Http\Request;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
        if ($request->is('admin/*')) {
            return true; // El panel de administración siempre devuelve JSON
        }

        return $request->expectsJson();
    });
})

Personalizar toda la respuesta

Con respond puedes procesar la respuesta generada.
use Symfony\Component\HttpFoundation\Response;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->respond(function (Response $response) {
        if ($response->getStatusCode() === 419) {
            return back()->with([
                'message' => 'La página ha caducado. Inténtalo de nuevo.',
            ]);
        }

        return $response;
    });
})

Clases de excepción personalizadas

Puedes crear tus propias excepciones en app/Exceptions/. Si defines report() y render(), se invocan automáticamente sin necesidad de configurarlas en bootstrap/app.php.

Crear la clase

1

Crear la excepción

php artisan make:exception InvalidOrderException
2

Implementar report() y render()

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class InvalidOrderException extends Exception
{
    public function __construct(
        private readonly int $orderId,
        string $message = 'Invalid order.',
    ) {
        parent::__construct($message);
    }

    /**
     * Reporta la excepción
     */
    public function report(): void
    {
        // Notificar a un servicio externo, etc.
    }

    /**
     * Convierte la excepción en respuesta HTTP
     */
    public function render(Request $request): Response
    {
        return response()->view('errors.invalid-order', [
            'orderId' => $this->orderId,
        ], 422);
    }
}
report() admite type-hints para inyección de dependencias. El contenedor de servicios de Laravel las resuelve automáticamente.

Interfaz ShouldntReport

Implementa ShouldntReport en excepciones que no deban reportarse. Las excepciones con esta interfaz no se reportan nunca.
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}

Lanzamiento de excepciones

Helper abort()

Puedes generar una respuesta HTTP de error desde cualquier punto de la aplicación.
// 404 Not Found
abort(404);

// Con mensaje
abort(403, 'No tienes permiso para realizar esta operación.');

abort_if() / abort_unless()

Helpers para lanzar excepciones condicionalmente.
// Aborta si $condition es true
abort_if(! $user->isAdmin(), 403);

// Aborta si $condition es false
abort_unless($user->hasPermission('edit'), 403, 'Permission denied.');
Muy útiles al comprobar permisos en controladores o middleware. Se combinan a menudo con gates y policies.

Control global de excepciones

Ignorar ciertas excepciones

Indica las excepciones a no reportar con dontReport. La lógica de renderizado personalizado sigue funcionando.
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})
Para ignorar según condición, pasa una closure a dontReportWhen.
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportWhen(function (Throwable $e) {
        return $e instanceof PodcastProcessingException &&
               $e->reason() === 'Subscription expired';
    });
})
Por defecto, Laravel ignora automáticamente ciertas excepciones: errores 404, token CSRF inválido (419), origen no coincidente (403), etc.

Reactivar excepciones ignoradas por Laravel

Para volver a reportar excepciones ignoradas por defecto, usa stopIgnoring.
use Symfony\Component\HttpKernel\Exception\HttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->stopIgnoring(HttpException::class);
})

Páginas de error HTTP

En Laravel puedes definir vistas de error personalizadas por código HTTP.

Crear vistas de error personalizadas

Crea plantillas Blade con el código de estado como nombre de archivo en resources/views/errors/.
resources/
└── views/
    └── errors/
        ├── 404.blade.php
        ├── 403.blade.php
        └── 500.blade.php
En la vista puedes usar la variable $exception para acceder a la información del error.
{{-- resources/views/errors/404.blade.php --}}
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Página no encontrada</title>
</head>
<body>
    <h1>404 - Página no encontrada</h1>
    <p>{{ $exception->getMessage() }}</p>
    <a href="{{ url('/') }}">Volver al inicio</a>
</body>
</html>

Publicar las plantillas de error por defecto

Para partir de las plantillas estándar de Laravel, publícalas con vendor:publish.
php artisan vendor:publish --tag=laravel-errors

Página de error de reserva

Como reserva cuando no hay una vista para un código concreto, puedes crear 4xx.blade.php y 5xx.blade.php.
resources/
└── views/
    └── errors/
        ├── 4xx.blade.php  # Fallback para códigos 4xx
        └── 5xx.blade.php  # Fallback para códigos 5xx
Para 404, 500 y 503, Laravel ya trae páginas por defecto. Para personalizarlas, crea archivos individuales (404.blade.php, etc.), no la de reserva.

Ejemplo práctico: handler de excepciones para API

En aplicaciones API, las excepciones deben devolverse siempre en JSON. Ejemplo de gestión centralizada de errores de API en bootstrap/app.php:
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    // Devuelve JSON para las solicitudes API
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Recurso no encontrado.',
            ], 404);
        }
    });

    $exceptions->render(function (AuthenticationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Se requiere autenticación.',
            ], 401);
        }
    });

    $exceptions->render(function (ValidationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Error de validación.',
                'errors'  => $e->errors(),
            ], 422);
        }
    });
})

Clase base para excepciones de API

Con una clase base para la API, puedes devolver respuestas uniformes en todos los endpoints.
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ApiException extends Exception
{
    public function __construct(
        string $message = 'An error occurred.',
        private readonly int $statusCode = 500,
        private readonly array $errors = [],
    ) {
        parent::__construct($message);
    }

    public function render(Request $request): JsonResponse
    {
        $data = ['message' => $this->getMessage()];

        if (! empty($this->errors)) {
            $data['errors'] = $this->errors;
        }

        return response()->json($data, $this->statusCode);
    }
}
Uso en el controlador:
<?php

namespace App\Http\Controllers\Api;

use App\Exceptions\ApiException;
use App\Models\Order;

class OrderController extends Controller
{
    public function show(int $id): JsonResponse
    {
        $order = Order::find($id);

        if (! $order) {
            throw new ApiException('Pedido no encontrado.', 404);
        }

        if ($order->isCancelled()) {
            throw new ApiException('Este pedido está cancelado.', 422);
        }

        return response()->json($order);
    }
}

Resumen

MétodoUso
$exceptions->report()Registra lógica de reporte personalizada por tipo
$exceptions->context()Añade contexto común a todos los logs de excepción
Método context()Aporta contexto propio en la clase de excepción
Helper report()Reporta sin interrumpir la respuesta
dontReportDuplicates()Evita duplicados por la misma instancia
Interfaz ShouldntReportCrea excepciones que nunca se reportan
MétodoUso
$exceptions->render()Devuelve una respuesta personalizada por tipo
Método render()Aporta la lógica de renderizado en la propia excepción
shouldRenderJsonWhen()Personaliza la detección JSON/HTML
respond()Procesa la respuesta generada
  • Basta con crear archivos como resources/views/errors/404.blade.php para que se usen automáticamente
  • Se accede al detalle del error con la variable $exception
  • php artisan vendor:publish --tag=laravel-errors publica las plantillas por defecto
  • Con 4xx.blade.php / 5xx.blade.php defines páginas de reserva
  • Configura siempre APP_DEBUG=false para no exponer la traza al usuario
  • Integra servicios externos de seguimiento de errores como Sentry o Flare para centralizar
  • Usa throttle() para evitar la saturación del log ante grandes volúmenes de excepciones
  • En los endpoints de API, mantén un formato JSON de error consistente
Última modificación el 13 de julio de 2026