In Laravel 13, how do you register a report callback for one exception type in bootstrap/app.php, and how do you keep that exception out of the default log?
answer
- withExceptions hands you an Exceptions object
- first parameter's type-hint picks the class
- chain ->stop() or return false
- callbacks run before the default logger
- exception's own report() method runs first
basics
~10 sInside withExceptions() in bootstrap/app.php, call $exceptions->report(function (SmsGatewayException $e) { ... }); the closure's type-hint selects the class. Laravel still logs the exception afterwards unless you chain ->stop() or the closure returns false.
solid answer
~40 sA Laravel 13 app has no `App\Exceptions\Handler`; reporting is configured in `bootstrap/app.php` through `->withExceptions(function (Exceptions $exceptions) { ... })`. `$exceptions->report(function (SmsGatewayException $e) { ... })` registers a callback, and Laravel reads the closure's first-parameter type-hint to decide which exceptions it receives, subclasses included. Matching callbacks run in registration order and, by default, the exception is still written to the default log afterwards. To make the callback the only destination, chain `->stop()` on the returned `ReportableHandler` or return `false` from the closure. An exception class can instead define its own `report()` method: Laravel calls it first, through the container, and unless it returns `false` neither the callbacks nor the default logging run.
code
php · 14 lines<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use App\Exceptions\SmsGatewayException;
use App\Support\OpsPager;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(web: __DIR__.'/../routes/web.php', health: '/up')
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->report(function (SmsGatewayException $e) {
app(OpsPager::class)->notify('SMS gateway: '.$e->getMessage());
})->stop(); // no default log entry for this type
})->create();go deeper
Know that bootstrap/app.php's withExceptions() is where reporting rules go, and that the closure's type-hint picks which exceptions it gets.
Explain the chain: the exception's own report(), then callbacks in order, then the default log, and how stop() or return false ends it.
Show you know filtering and throttling run before any callback, so a callback cannot see dropped exceptions, and catch the void report() trap in review.
Frame where reporting rules live: in bootstrap/app.php for cross-cutting routing, on the exception class when the logic belongs to that failure.
## Where reporting is configured in Laravel 13 **Reporting** is the half of Laravel's exception handling that records a failure: writing it to the log or sending it somewhere else. The other half, **rendering**, turns the exception into the HTTP response and is a separate step. Since the slim skeleton of Laravel 11, a new application has no `app/Exceptions/Handler.php`. The framework's `Illuminate\Foundation\Exceptions\Handler` is configured from `bootstrap/app.php`: ```php ->withExceptions(function (Exceptions $exceptions): void { // reporting and rendering rules go here }) ``` The `$exceptions` argument is an `Illuminate\Foundation\Configuration\Exceptions` object that forwards each call to the handler. ## Registering a report callback `$exceptions->report(callable $using)` registers a **report callback** and returns an `Illuminate\Foundation\Exceptions\ReportableHandler`. Laravel never asks you for a class name. It reflects on the closure and uses the **type-hint of its first parameter**: - `function (SmsGatewayException $e)` receives `SmsGatewayException` and every subclass, because the match is an `is_a()` check. - A union type such as `SmsGatewayException|MailTransportException $e` matches either class. - `function (Throwable $e)` receives everything that reaches the callback stage. - A closure with **no type-hint** fails: when the handler tries to match it, it throws a `RuntimeException` saying the first parameter is missing a type hint. `$exceptions->reportable()` is an alias for the same call. The callback applies wherever the handler reports: exceptions escaping HTTP requests, failed queued jobs reported by the worker, Artisan commands and explicit `report($e)` calls all pass through the same chain, so one registration covers every entry point. ## What happens after the callback By default a report callback **adds** a destination; it does not replace the log. After your callback returns, Laravel continues to the next matching callback and finally writes the exception to the default logger. Two ways end the chain: | You write | Effect | |---|---| | `$exceptions->report(fn (SmsGatewayException $e) => ...)` | Callback runs, later callbacks run, default log entry written | | `...->stop()` on the returned handler | Callback runs, then reporting ends | | `return false;` inside the callback | Same as `stop()`, decided at runtime | | Any other return value (`true`, `null`, a response) | Ignored; reporting continues | Returning a response from a report callback does nothing for the user: rendering is a separate step that runs after reporting. ## Reportable exceptions: a report() method on the class Instead of a callback in `bootstrap/app.php`, an exception class may define a public **`report()`** method. Laravel calls it through the service container, so its parameters can type-hint services that are injected for you. Its return value is checked strictly against `false`: - returning `false` means "not handled here": the registered callbacks and the default log still run; - returning anything else, **including nothing** from a `void` method, means handled: the callbacks and the default log are both skipped. That last rule is the classic surprise. A developer adds `public function report(): void` to send a notification and, without meaning to, removes the exception from the log. ## The order the handler follows When `report()` is called on the handler, whether by the framework for an uncaught exception or by the `report()` helper, it works through these steps: 1. Decide whether to report at all: duplicate instances (when deduplication is on), the `ShouldntReport` interface, the `dontReport` lists, `dontReportWhen` closures, then `throttle()`. A filtered exception stops here. 2. Call the exception's own `report()` method, if it has one, and stop unless it returned `false`. 3. Call each matching report callback in registration order, and stop at the first that returns `false` or was marked with `stop()`. 4. Write the exception to the default logger, at the level `level()` maps for it (`error` by default), with its context array. Step 1 matters in practice: an exception you put in `dontReport`, or that `throttle()` drops, never reaches your callback, so a callback cannot count or forward it. ## Common mistakes - Expecting a callback to replace logging without `stop()` or `return false`, then seeing each failure twice. - Writing the closure without a type-hint. - Giving the exception a `void` `report()` method and losing the log entry. - Putting the class in `dontReport` "so only the callback handles it"; the callback never runs. - Looking for `App\Exceptions\Handler::register()` in a Laravel 13 app; that file belongs to Laravel 10 and earlier.
- What happens when the exception class defines a report() method declared void?Laravel calls it through the container and treats every return value except `false` as handled. A `void` method returns `null`, so reporting ends there: no registered report callback runs and nothing is written to the default log. Declare it `bool` and return `false` when the normal path should continue.
- Does a report callback run for an exception listed in dontReport or dropped by throttle()?No. The handler first decides whether to report at all, checking deduplication, `ShouldntReport`, the `dontReport` lists, `dontReportWhen` closures and `throttle()`. Only an exception that passes reaches its own `report()` method, the callbacks and the logger.
- Does a report callback change the response the user sees?No. Reporting and rendering are separate steps of Laravel's handler. A report callback's return value only decides whether reporting continues; the response comes from render callbacks or the handler's defaults.
saying these in an interview costs you the question
- Registering a report callback automatically stops the default log entry
- The report closure can omit its type-hint and will receive every exception
- A void report() method on the exception still lets default logging run
- Returning a response from a report callback changes what the user sees
- Adding the class to dontReport leaves only the custom callback running
- A new Laravel 13 app registers callbacks in App\Exceptions\Handler::register()