[Guía completa y comprobada en campo] Manejo de errores e incidentes en Laravel — Diseño de excepciones, páginas de error, errores de API, logging/monitorización, reintentos, mantenimiento y rutas de recuperación accesibles
Lo que aprenderás en este artículo (puntos clave)
- Cómo dar forma al manejo de excepciones de Laravel (
Handler,reportable/renderable) para que sea robusto en operaciones - Qué significan errores comunes como 404/419/429/500/503 y cómo diseñar páginas de error amigables para el usuario
- Cómo unificar formatos de error de API (problem+json) y acortar investigaciones con trace IDs
- Logging estructurado, enmascaramiento de PII, monitorización/alertas y primera respuesta ante incidentes (runbooks)
- Manejo de reintentos y “fallbacks”, además de fallos en colas y APIs externas
- Guía accesible que evita que los usuarios se pierdan incluso durante errores (foco/anuncios/sin señales solo por color/acciones siguientes claras)
Lectores objetivo (¿a quién beneficia?)
- Ingenieros Laravel de nivel principiante–intermedio: Quieren pasar de un manejo de excepciones “solo que funcione” a implementaciones resilientes
- Tech leads / responsables de operaciones: Quieren mejorar monitorización y logs para reducir tiempo de investigación y MTTR
- Diseñadores / redactores / QA: Quieren consistencia en textos y flujos de reintento/recuperación claros para todo el mundo
- Responsables de integraciones API: Quieren formatos de error amigables para clientes e IA de información que reduzca consultas a soporte
Nivel de accesibilidad: ★★★★★
Incluye ejemplos concretos: encabezados/resúmenes/acciones siguientes,
role="alert"/role="status", gestión de foco, mensajes no dependientes del color, guía de mantenimiento y UI de reintento.
1. Introducción: los sistemas se vuelven más fuertes cuando diseñas los errores como inevitables
Las partes más difíciles de la respuesta a incidentes son (1) no conocer la causa y (2) que los usuarios se pierdan. Laravel tiene mecanismos sólidos de manejo de excepciones, pero si se deja “tal cual” a menudo queda en un estado como: “los logs no tienen suficiente contexto”, “todos los 500 se ven iguales”, “la API y la UI web tienen formas distintas de error” y “las páginas de error no ayudan”.
En esta guía tratamos los errores no como simples fallos sino como funcionalidades que explican qué ocurrió y guían la recuperación, y mostramos un patrón que combina implementación, operaciones y accesibilidad.
2. Política de diseño: clasifica los errores en tres grupos
Una clasificación práctica y “amigable para producción” es:
- Corregible por el usuario (errores de entrada, permisos insuficientes, sesión caducada)
- Ejemplos: 422, 401/403, 419
- Comportamiento deseado: explicación breve de qué corregir + siguiente paso claro
- Temporal / esperar y reintentar (limitación de tasa, fallos de API externa, mantenimiento)
- Ejemplos: 429, 503
- Comportamiento deseado: tiempo de espera, método de reintento y alternativas
- No corregible por el usuario (bugs, estados inesperados, fallos del servidor)
- Ejemplo: 500
- Comportamiento deseado: disculpa + alcance/impacto + ID de consulta + ruta de recuperación
Cuando alineas la copy de UI, el estado HTTP, los logs y las alertas con esta clasificación, la comunicación del equipo se acelera mucho.
3. Fundamentos del manejo de excepciones en Laravel: haz explícitas las responsabilidades del Handler
En Laravel, el punto de entrada principal es app/Exceptions/Handler.php. En general quieres dos responsabilidades:
report: para operaciones (logging, monitorización, notificar a Sentry, etc.)render: para usuarios (formatear respuestas de pantalla/JSON)
3.1 Decide qué excepciones no reportar
Si cada fallo de validación o autorización dispara alertas, te ahogarás en ruido. Las señales valiosas para operaciones suelen ser “inesperadas” y “picos repentinos”.
Usa dontReport (o condiciones vía reportable) para limitar lo que se reporta.
// app/Exceptions/Handler.php (extracto)
protected $dontReport = [
\Illuminate\Validation\ValidationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
];
4. Acorta “soporte → investigación” con trace IDs
Cuando un usuario dice “me salió un error”, el peor caso es no poder reproducirlo. Así que adjunta un trace_id (o request_id) a cada request y devuélvelo tanto en la UI como en respuestas de API.
4.1 Adjuntar un trace ID con middleware
// app/Http/Middleware/TraceId.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Str;
class TraceId
{
public function handle($request, Closure $next)
{
$traceId = $request->header('X-Trace-Id') ?: 'req-'.Str::uuid()->toString();
$request->attributes->set('trace_id', $traceId);
$response = $next($request);
$response->headers->set('X-Trace-Id', $traceId);
return $response;
}
}
- Aplícalo tanto a
webcomo aapipara que investigar sea mucho más fácil. - También puedes mostrarlo en pantalla: “Si contactas a soporte, comparte este número”.
5. UI web: diseña páginas de error como “rutas de recuperación”
5.1 404 (No encontrado)
Esto puede ser error del usuario o un enlace roto. Haz tres cosas:
- Qué pasó (página no encontrada)
- Qué hacer después (inicio, buscar, volver)
- Si puedes, una pista de causa (la URL puede haber cambiado)
{{-- resources/views/errors/404.blade.php --}}
@extends('layouts.app')
@section('title','Page Not Found')
@section('content')
<main aria-labelledby="error-title">
<h1 id="error-title" tabindex="-1">Page Not Found</h1>
<p>The URL may have changed, or it might have been typed incorrectly.</p>
<ul>
<li><a href="{{ route('home') }}">Back to home</a></li>
<li><a href="{{ route('products.index') }}">Browse products</a></li>
</ul>
</main>
@endsection
Notas de accesibilidad
- Añade
tabindex="-1"al heading para poder mover el foco ahí tras la navegación. - No dependas solo del color; explica en texto claro.
5.2 419 (Page Expired: expiración de CSRF/sesión)
Esto suele requerir reenviar un formulario. La clave es copy no culpabilizadora y pasos de recuperación claros.
- “Tu sesión expiró por inactividad. Inténtalo de nuevo.”
- Aclara que la entrada puede haberse perdido; si puedes, guía hacia borradores/restauración.
5.3 429 (Too Many Requests: limitación de tasa/sobrecarga)
Comunica que se recuperará si esperan y muestra segundos si puedes derivarlo de Retry-After.
5.4 503 (Mantenimiento/caída temporal)
Sé explícito sobre: “cuándo vuelve”, “qué está afectado” y “opciones de contacto urgente”.
Además, mantén la página de mantenimiento ligera: evita imágenes pesadas y JS complejo por estabilidad.
6. APIs: unifica formatos de error (problem+json)
Las APIs se juzgan por “código de estado + body”. Si los bodies varían, el manejo de excepciones del cliente se dispara.
Una recomendación fuerte es RFC 7807: application/problem+json.
6.1 Ejemplo: error de validación (422)
{
"type": "https://example.com/problems/validation",
"title": "Validation Failed",
"status": 422,
"detail": "Please check your input.",
"errors": {
"email": ["Email is required."]
},
"trace_id": "req-..."
}
6.2 Unificar respuestas JSON en Handler (alto nivel)
// app/Exceptions/Handler.php (ejemplo esquemático)
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
$traceId = $request->attributes->get('trace_id');
$status = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
$payload = [
'type' => $this->problemType($e, $status),
'title' => $this->problemTitle($status),
'status' => $status,
'detail' => $this->problemDetail($e, $status),
'trace_id' => $traceId,
];
if ($e instanceof ValidationException) {
$payload['type'] = 'https://example.com/problems/validation';
$payload['status'] = 422;
$payload['title'] = 'Validation Failed';
$payload['detail'] = 'Please check your input.';
$payload['errors'] = $e->errors();
}
return response()->json($payload, $payload['status'])
->header('Content-Type', 'application/problem+json');
}
return parent::render($request, $e);
}
Notas
- Haz que
typesea una URL estable vinculada a una página de explicación para alinear soporte e ingeniería. - Devolver
trace_idreduce idas y vueltas con soporte.
7. Diseño de logs: el lector eres tú en el futuro
7.1 Usa logs estructurados
Los logs se buscan mucho mejor como key–value que como prosa.
Log::error('api.failed', [
'trace_id' => request()->attributes->get('trace_id'),
'user_id' => optional(auth()->user())->id,
'path' => request()->path(),
'method' => request()->method(),
'status' => 500,
'exception' => get_class($e),
]);
7.2 Enmascara PII
Registrar emails/direcciones tal cual es riesgoso. Una base segura es “registrar solo IDs” y enmascarar el resto si de verdad es necesario.
8. Fallos de APIs externas: timeouts, reintentos y fallbacks
Las APIs externas se caen. Si construyes asumiéndolo, es menos probable que una caída se vuelva fatal.
8.1 Básicos del cliente HTTP
- Define timeouts explícitamente
- Reintenta con backoff exponencial
- Decide dónde puedes “degradar con gracia” en lugar de romper toda la funcionalidad
$res = Http::timeout(10)
->retry(3, 200, function ($exception, $request) {
return true; // idealmente, limitar por condiciones
})
->get('https://api.example.com/data');
if ($res->failed()) {
// Ejemplo: devolver valor previo en caché (fallback)
$cached = Cache::get('external:data');
return $cached ? $cached : null;
}
Cache::put('external:data', $res->json(), 300);
return $res->json();
9. Fallos de colas/jobs: reintentos y mentalidad de “dead-letter”
- Fallos temporales (red) → reintentar
- Fallos permanentes (datos malos) → aislar como fallo y que humanos revisen
Los jobs en Laravel se estabilizan en ops cuando defines explícitamente tries/backoff/timeout.
class SendInvoiceMail implements ShouldQueue
{
public $tries = 5;
public $timeout = 120;
public function backoff(): array
{
return [10, 30, 60, 120, 300];
}
public function handle()
{
// lógica de envío
}
}
Ángulo accesible (de cara al usuario)
- Comunica breve: “Enviando…” → “Listo” → “Falló (reintentar/contactar).”
- Usa
role="status"para progreso/resultado y que lectores de pantalla anuncien cambios.
10. UI de error: el “mínimo” para evitar que el usuario se pierda
El mínimo en pantalla:
- Encabezado: qué ocurrió
- Resumen: 1–2 frases
- Siguiente acción: links/botones
- Contacto:
trace_id(si hace falta)
10.1 Usa role="alert" para errores críticos
Para resúmenes de errores en formularios, role="alert" ayuda—pero evita abusar; resérvalo para lo realmente urgente.
@if(session('error'))
<div role="alert" class="border p-3">
{{ session('error') }}
</div>
@endif
10.2 Usa role="status" para fallos durante carga
Ofrece “Recargar”, “Intentar de nuevo” y “Modo liviano” para reducir callejones sin salida.
11. Monitorización y alertas: qué vigilar para detectar problemas
Un set mínimo recomendado:
- Tasa de 5xx (detección de picos)
- Tasa de 429 (pico de tráfico o throttling mal configurado)
- Tasa de fallos de API externa y conteo de timeouts
- Latencia de cola (crecimiento del backlog)
- Consultas lentas de DB
Demasiadas alertas se ignoran. Empieza con:
- Picos de 5xx
- Deterioro de demora de cola
- Aumento fuerte de tasa de fallos en APIs externas
12. Prepara un runbook (procedimientos de primera respuesta)
La respuesta a incidentes se vuelve más tranquila cuando hay procedimiento. Mantenlo corto, pero decide:
- Revisar alcance de impacto (qué funcionalidades, qué usuarios)
- Puntos de entrada para búsqueda en logs (
trace_id, endpoint, clase de excepción) - Verificar diffs de deploy recientes
- Criterios de rollback
- Plantillas de comunicación a usuarios (mensajes de mantenimiento/estado)
13. Testing: trata los errores como “spec” y fíjalos
13.1 Ejemplo de feature test (422)
public function test_api_validation_problem_json()
{
$res = $this->postJson('/api/v1/users', ['email' => '']);
$res->assertStatus(422)
->assertHeader('Content-Type', 'application/problem+json')
->assertJsonStructure(['type','title','status','detail','errors','trace_id']);
}
13.2 Incluye 429 y 503 en cobertura
- Rate limiting adjunta
Retry-After - Modo mantenimiento devuelve la página esperada
- Páginas de error web incluyen “siguiente acción”
14. Errores comunes y cómo evitarlos
- Tragar excepciones (
try/catchcon cuerpos vacíos)- Evitar: si lo tragas, acompáñalo siempre con fallback + logging
- Un solo mensaje genérico de 500
- Evitar: variar copy y rutas por clasificación (corregible / esperable / no corregible)
- Logs sin info o con demasiado
- Evitar: estandariza claves mínimas (
trace_id+ campos core); no registres PII
- Evitar: estandariza claves mínimas (
- Formas de error divergentes entre API y UI
- Evitar: unifica API con problem+json; prioriza rutas de recuperación en UI
- Infierno de alertas
- Evitar: empieza con picos y señales de alta severidad; introduce gradualmente
15. Checklist (para compartir)
Excepciones / respuestas
- [ ] Adjuntar
trace_ida todas las respuestas - [ ] Unificar formato de API como
application/problem+json - [ ] Proveer páginas 404/419/429/500/503 con acciones siguientes claras
Logs / ops
- [ ] Logs estructurados (trace_id, user_id, path, status, exception)
- [ ] Política de enmascaramiento de PII (primero IDs)
- [ ] Monitorizar métricas clave (5xx, 429, fallos de API externa, latencia de cola)
- [ ] Preparar un runbook conciso (primera respuesta)
Fiabilidad
- [ ] Definir timeout/retry/fallback para APIs externas
- [ ] Definir tries/backoff/timeout explícitos para jobs
- [ ] Preparar guía de modo mantenimiento (503)
Accesibilidad
- [ ] Encabezado + resumen + siguiente acción + ID de consulta
- [ ]
role="alert"para errores críticos;role="status"para progreso - [ ] Sin señales solo por color; ruta de recuperación navegable por teclado
16. Conclusión: convierte los errores en “guía para la recuperación”
- Clasifica errores y alinea copy de UI, HTTP, logs y monitorización para ser resiliente.
- Solo añadir un
trace_idglobal puede acelerar muchísimo la investigación. - Unifica APIs con problem+json para simplificar el manejo del lado cliente.
- Las páginas de error nunca deben ser un callejón sin salida: siempre ofrece acciones siguientes.
- Trata APIs externas, colas y sobrecarga como “esperables” y explica el estado con claridad al usuario.
- La accesibilidad importa más durante incidentes: haz que la guía calma y navegable sea el estándar.
Referencias
- Laravel oficial
- Especificaciones / guías
- Accesibilidad
