Every mechanism in this chapter depends on something being up: Nightwatch, the queue, the shared log store. None of them are guaranteed to be. The fleet's answer isn't to make logging infallible, it's to make sure logging failing doesn't take the request down with it.
Look again at the report() closure from earlier in this chapter:
try {
// ...Nightwatch calls...
} catch (Throwable) {
// Nightwatch not available or container not initialized
}
That try/catch exists because reporting an exception can itself throw, and a report() closure that fatals while reporting a failure is the worst possible outcome: the original error is lost, and now there's a second one on top of it. Swallow it. The client still gets their ErrorResponse, unaffected.
This isn't a hypothetical guard against some future outage, either. Point back at the severity split from earlier in this chapter: Nightwatch::warning(), Nightwatch::info(), and Nightwatch::captureException() throw on every single call, because the installed Nightwatch facade doesn't define them. This exact catch is what's quietly eating that. The request doesn't slow down. The client never notices. But the severity classification that closure was written to provide isn't happening, and nothing surfaces that fact, because the one thing built to catch a silent failure is the thing silently failing.
SendToLogsJob gets the same treatment structurally, just earlier in the pipeline. LogApiRequestsMiddleware calls $next($request) and returns the response before defer() runs. The request log ships after the response has already gone out. If the queue is backed up, if the log store is down, the caller never sees it, they already have their answer. The job is ShouldQueue and ShouldBeEncrypted, so it queues durably instead of running inline: a slow or unavailable log sink degrades your logging, not your API.
api-license's dontReport(ConnectionException::class) closes the loop from the other direction: a downstream timeout isn't reported as an application error at all, it's rendered straight to a 504 and left alone. The rule underneath all three of these: observability is a passenger, not the driver. If capturing what went wrong can crash the thing that's already wrong, you've built the exact failure mode you were trying to catch.
Summary
- [X] Errors go through one door.
withExceptions()inbootstrap/app.php, not per-controller try/catch, not aHandlerclass this fleet doesn't have anymore. - [X] Rendering is shared, not copied.
ApiExceptionRendererandErrorResponselive inapi-infrastructureonce, and every app calls the same code. - [X] A status code is not a contract. Two different
404s look identical without a machine-readable code riding alongside the message. - [X] Context lives on the exception, not the log call. A boolean like
$outcomeUnknownsays more than any stack trace. - [X] Correlation is only as good as your join key. Without a dedicated trace ID, the bearer and a tight time window are what you've actually got.
- [X] Logging must be able to fail without failing the request. Catch around reporting, defer the log write, queue it, don't let observability become one more thing that breaks.
An error handler that only handles the happy path's evil twin isn't finished. It's finished when it survives its own dependencies going down.