NightFox

NightFox

Error Handling, Logging, and Observability

When something breaks, the user should see a calm, safe error page, and whoever has to fix it (usually me) should find enough in the logs to understand what happened. Most apps get one of those right. You want both.

Separate expected and unexpected failures

FailureExampleResponse
ValidationMissing required emailReturn field errors; do not log as a system failure.
AuthorizationUser lacks refund permissionReturn 403 and record a security event when appropriate.
Domain conflictAttempt to cancel a completed tripReturn a clear conflict or validation response.
InfrastructureDatabase unavailableReturn a generic 500/503, alert, and preserve detailed logs.
Programming defectUnexpected null or type errorReturn 500, capture stack trace, and fix the defect.

Global exception handler

<?php
declare(strict_types=1);

set_exception_handler(static function (Throwable $exception): void {
    $requestId = $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8));

    error_log(json_encode([
        'level' => 'error',
        'event' => 'unhandled_exception',
        'request_id' => $requestId,
        'type' => $exception::class,
        'message' => $exception->getMessage(),
        'file' => $exception->getFile(),
        'line' => $exception->getLine(),
        'trace' => $exception->getTraceAsString(),
    ], JSON_UNESCAPED_SLASHES));

    http_response_code(500);
    header('Content-Type: text/plain; charset=utf-8');
    echo "The application encountered an error. Reference: {$requestId}";
});

Structured logging

{
  "timestamp": "2026-07-16T13:41:22Z",
  "level": "warning",
  "event": "payment_gateway_timeout",
  "request_id": "req_7c761f23",
  "invoice_id": 214,
  "duration_ms": 5002,
  "attempt": 2
}

Structured fields are what make logs searchable. Keep the message short and the same every time, and put the values that change (IDs, amounts, durations) in their own fields. Then you can actually find "every failed payment for customer 42" later.

Do not log secrets

  • Passwords or password reset tokens
  • Session identifiers
  • API keys and authorization headers
  • Full payment-card information
  • Raw database connection strings
  • Entire request bodies by default

Useful measurements

I like to track request time, database time, time spent waiting on outside APIs, queue delay, errors, retries, and the business outcomes that matter. A page can return a happy HTTP 200 and still not do its job. Business metrics (orders placed, emails sent) are how you catch that.

Production debugging workflow

  1. Capture the exact time, user-visible reference, route, and action.
  2. Find the request in reverse-proxy and application logs.
  3. Inspect correlated database, queue, and external-service events.
  4. Reproduce with the same input in a safe environment.
  5. Fix the root cause and add a regression test.
  6. Document the failure mode and monitoring improvement.

Last Modified: October 12, 2026, 11:46 am