php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

[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 web como a api para 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 type sea una URL estable vinculada a una página de explicación para alinear soporte e ingeniería.
  • Devolver trace_id reduce 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/catch con 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
  • 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_id a 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_id global 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

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)