php elephant sticker
Photo by RealToughCandy.com on Pexels.com
Table of Contents

[Field-Proof Complete Guide] Laravel Error Handling and Incident Response — Exception Design, Error Pages, API Errors, Logging/Monitoring, Retries, Maintenance, and Accessible Recovery Paths

What you’ll learn in this article (key points)

  • How to shape Laravel exception handling (Handler, reportable/renderable) into an operations-strong form
  • What common errors like 404/419/429/500/503 mean, and how to design user-friendly error pages
  • How to unify API error formats (problem+json) and shorten investigations with trace IDs
  • Structured logging, PII masking, monitoring/alerts, and first-response incident handling (runbooks)
  • Handling retries and fallbacks, plus queue/external API failures
  • Accessible guidance that prevents users from getting lost—even during errors (focus/announcements/no color-only cues/clear next actions)

Target readers (who benefits?)

  • Beginner–intermediate Laravel engineers: Want to graduate from “just make it work” exception handling and build resilient implementations
  • Tech leads / operations owners: Want to improve monitoring and logs to reduce investigation time and MTTR
  • Designers / writers / QA: Want consistent wording and retry/recovery flows that are clear to everyone
  • API integration owners: Want client-friendly error formats and information architecture that reduces support requests

Accessibility level: ★★★★★

Includes concrete examples: error headings/summaries/next actions, role="alert"/role="status", focus management, non-color-dependent messaging, maintenance guidance, and retry UI.


1. Introduction: Systems get stronger when you design for errors as inevitable

The hardest parts of incident response are (1) not knowing the cause, and (2) users getting lost. Laravel has solid exception-handling mechanisms, but if left as-is it often ends up in a state like: “logs don’t have enough context,” “all 500s look the same,” “API and web UIs have different error shapes,” and “error pages are unhelpful.”

In this guide, we treat errors not as mere failures but as features that explain what happened and guide recovery, and we show a pattern that combines implementation, operations, and accessibility.


2. Design policy: Classify errors into three groups

A field-friendly classification is:

  • User-fixable (input mistakes, insufficient permissions, expired sessions)
    • Examples: 422, 401/403, 419
    • Desired behavior: brief explanation of what to fix + clear next step
  • Temporary / wait-and-retry (throttling, external API failures, maintenance)
    • Examples: 429, 503
    • Desired behavior: provide wait time, retry method, and alternatives
  • Not user-fixable (bugs, unexpected states, server failures)
    • Example: 500
    • Desired behavior: apology + scope/impact + inquiry ID + recovery path

When you align the UI copy, HTTP status, logs, and alerts to this classification, team communication becomes much faster.


3. Laravel exception-handling basics: Make the Handler’s responsibilities explicit

In Laravel, the main entry point is app/Exceptions/Handler.php. You generally want two responsibilities:

  • report: for operations (logging, monitoring, notifying Sentry, etc.)
  • render: for users (formatting screen/JSON responses)

3.1 Decide which exceptions not to report

If every validation or authorization failure triggers alerting, you’ll drown in noise. Operationally valuable signals are mainly “unexpected” and “sudden spikes.”
Use dontReport (or conditions via reportable) to narrow what gets reported.

// app/Exceptions/Handler.php (excerpt)
protected $dontReport = [
    \Illuminate\Validation\ValidationException::class,
    \Illuminate\Auth\Access\AuthorizationException::class,
    \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
];

4. Shorten “support → investigation” with trace IDs

When a user says “I got an error,” the worst case is being unable to reproduce it. So attach a trace_id (or request_id) to every request, and return it in both UI and API responses.

4.1 Attach a trace ID via 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;
    }
}
  • Apply this to both web and api to make investigations dramatically easier.
  • You can also display it on screen: “If you contact support, please share this number.”

5. Web UI: Design error pages as “recovery paths”

5.1 404 (Not Found)

This is either user error or a broken link. Do three things:

  • What happened (page not found)
  • What to do next (home, search, back)
  • If possible, a cause hint (URL may have changed)
{{-- 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

Accessibility notes

  • Add tabindex="-1" to the heading so you can move focus there after navigation.
  • Don’t rely on color alone; explain in plain text.

5.2 419 (Page Expired: CSRF/session expiration)

This often requires resubmitting a form. The key is non-blaming copy and clear recovery steps.

  • “Your session expired due to inactivity. Please try again.”
  • Clarify that input may be lost; if you can, guide users toward drafts/restore.

5.3 429 (Too Many Requests: throttling/overload)

Communicate that it will recover if they wait, and show seconds if you can derive Retry-After.

5.4 503 (Maintenance/temporary downtime)

Be explicit about: “when it returns,” “what’s affected,” and “urgent contact options.”
Also keep the maintenance page lightweight—avoid heavy images and complex JS for stability.


6. APIs: Unify error formats (problem+json)

APIs are judged by “status code + body.” If bodies vary, client-side exception handling balloons.
A strong recommendation is RFC 7807: application/problem+json.

6.1 Example: validation error (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 Unify JSON responses in Handler (high-level)

// app/Exceptions/Handler.php (outline example)
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);
}

Notes

  • Make type a stable URL tied to an explanation page so support and engineering stay aligned.
  • Returning trace_id reduces back-and-forth in support.

7. Log design: The reader is your future self

7.1 Use structured logs

Logs are far more searchable as key–value than prose.

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 Mask PII

Logging emails/addresses verbatim is risky. A safe baseline is “log IDs only,” and mask anything else if truly needed.


8. External API failures: timeouts, retries, and fallbacks

External APIs go down. If you build with that assumption, outages are less likely to become fatal.

8.1 HTTP client basics

  • Set timeouts explicitly
  • Retry with exponential backoff
  • Decide where you can “degrade gracefully” instead of breaking the whole feature
$res = Http::timeout(10)
  ->retry(3, 200, function ($exception, $request) {
      return true; // ideally constrain by conditions
  })
  ->get('https://api.example.com/data');

if ($res->failed()) {
    // Example: return cached previous value (fallback)
    $cached = Cache::get('external:data');
    return $cached ? $cached : null;
}
Cache::put('external:data', $res->json(), 300);
return $res->json();

9. Queue/job failures: retries and “dead-letter” thinking

  • Temporary failures (network) → retry
  • Permanent failures (bad data) → isolate as failure and have humans review

Laravel jobs become more stable in ops when you explicitly set 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()
    {
        // sending logic
    }
}

Accessible angle (user-facing)

  • Communicate briefly: “Sending…” → “Done” → “Failed (retry/contact).”
  • Use role="status" for progress/result so screen readers can announce updates.

10. Error UI: The “minimum set” to prevent users from getting lost

The minimum set on screen:

  • Heading: what happened
  • Summary: 1–2 sentences
  • Next action: links/buttons
  • Contact info: trace_id (if needed)

10.1 Use role="alert" for critical errors

For form error summaries, role="alert" helps—but avoid overuse; reserve it for genuinely urgent parts.

@if(session('error'))
  <div role="alert" class="border p-3">
    {{ session('error') }}
  </div>
@endif

10.2 Use role="status" for loading failures

Offer “Reload,” “Try again,” and “Use lightweight mode” to reduce dead ends.


11. Monitoring and alerts: What to watch so you can notice issues

A minimal recommended set:

  • 5xx rate (spike detection)
  • 429 rate (traffic surge or incorrect throttling)
  • External API failure rate and timeout count
  • Queue latency (backlog growth)
  • DB slow queries

Too many alerts leads to being ignored. Start with:

  • 5xx spikes
  • Queue delay deterioration
  • Major external API failure-rate increase

12. Prepare a runbook (first-response procedures)

Incident response becomes calmer when there’s a procedure. Keep it short, but decide:

  • Check impact scope (which features, which users)
  • Log search entry points (trace_id, endpoint, exception class)
  • Verify recent deploy diffs
  • Rollback criteria
  • User comms templates (maintenance/status page messaging)

13. Testing: Treat errors as “spec” and lock them in

13.1 Feature test example (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 Include 429 and 503 in test coverage

  • Rate limiting attaches Retry-After
  • Maintenance mode returns the intended page
  • Web error pages include a “next action”

14. Common pitfalls and how to avoid them

  • Swallowing exceptions (try/catch with empty bodies)
    • Avoidance: if you swallow it, always pair with fallback behavior and logging
  • Only one generic 500 message
    • Avoidance: vary copy and routes by classification (fixable / waitable / not fixable)
  • Logs either lack info or have too much
    • Avoidance: standardize minimum keys (trace_id + core fields); don’t log PII
  • API and UI error shapes diverge too much
    • Avoidance: unify API via problem+json; prioritize recovery paths on UI
  • Alert hell
    • Avoidance: start with spikes and high-severity signals; introduce gradually

15. Checklist (for distribution)

Exceptions / responses

  • [ ] Attach trace_id to all responses
  • [ ] Unify API format as application/problem+json
  • [ ] Provide 404/419/429/500/503 pages with clear next actions

Logs / ops

  • [ ] Structured logs (trace_id, user_id, path, status, exception)
  • [ ] PII masking policy (ID-first)
  • [ ] Monitor key metrics (5xx, 429, external API failures, queue latency)
  • [ ] Prepare a concise runbook (first response)

Reliability

  • [ ] Define timeout/retry/fallback for external APIs
  • [ ] Explicit tries/backoff/timeout for jobs
  • [ ] Prepare maintenance-mode (503) guidance

Accessibility

  • [ ] Heading + summary + next action + inquiry ID
  • [ ] role="alert" for critical errors; role="status" for progress
  • [ ] No color-only cues; keyboard-navigable recovery path

16. Conclusion: Grow errors into “guidance for recovery”

  • Classify errors and align UI copy, HTTP, logs, and monitoring to become resilient.
  • Just adding a system-wide trace_id can dramatically speed investigations.
  • Unify APIs with problem+json to simplify client-side handling.
  • Error pages must never be dead ends—always provide next actions.
  • Treat external APIs, queues, and overload as “waitable” and explain status clearly to users.
  • Accessibility matters most during incidents—make calm, navigable guidance the default.

References

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

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