skip to content

In Laravel 13, how do you customise the response for one exception type with a render callback, a render() method on the exception, or respond()?

level: middleimportance: must knowfreq 50%

answer

  1. $exceptions->render(fn (Type $e, Request $r))
  2. null falls through to the default
  3. exception's own render($request) runs first
  4. false from render() means use the default
  5. respond() rewrites every final response

basics

~20 s

Register $exceptions->render(function (SeatUnavailableException $e, Request $request) { ... }) in bootstrap/app.php, or give the exception a render($request) method. Return a response to use it, or null/false for the default; respond() adjusts every final exception response.

solid answer

~40 s

Rendering in Laravel 13 is configured in `bootstrap/app.php`'s `withExceptions()`. `$exceptions->render(function (SeatUnavailableException $e, Request $request) { ... })` registers a render callback matched by its first-parameter type-hint; it receives the exception and the request, and the first matching callback that returns non-null wins. Returning `null` falls through to later callbacks and then Laravel's default rendering. An exception class can instead define `render(Request $request)`: it runs before any callback, and its return (a response, view, array or string) is used unless it is falsy, so `return false` means default rendering. `$exceptions->respond(fn (Response $response, Throwable $e, Request $request) => ...)` sees the final response from every path and must return a response; it is a single hook, so a second `respond()` call replaces the first. Rendering never affects reporting.

code

php · 25 lines
php
<?php

use App\Exceptions\SeatUnavailableException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(web: __DIR__.'/../routes/web.php', health: '/up')
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->render(function (SeatUnavailableException $e, Request $request) {
            if ($request->expectsJson()) {
                return response()->json(['message' => "Seat {$e->seat} was just taken."], 409);
            }

            return back()->with('status', "Seat {$e->seat} was just taken. Please pick another.");
        });

        $exceptions->respond(function (Response $response) {
            $response->headers->set('Cache-Control', 'no-store');

            return $response;
        });
    })->create();

go deeper

for a junior

Know that bootstrap/app.php can give one exception type its own response with $exceptions->render().

for a middle

Explain type-hint matching, null fall-through, the exception's render() running first, and what respond() is for.

for a senior

Choose between class-level and central rendering, keep respond() to one well-tested callback, and keep rendering separate from reporting.

for a principal

Decide where presentation of domain failures lives so web and API teams do not duplicate or contradict each other.

## Three hooks, three scopes Laravel's exception handler turns an uncaught exception into an HTTP response. By default it renders error views or a JSON body; three hooks let you change that, each with a different scope. | Hook | Where | Scope | Return to fall back | |---|---|---|---| | `render()` method on the exception | the exception class | that class and subclasses | `false` or `null` | | `$exceptions->render(fn ...)` | `bootstrap/app.php` | any type, chosen by type-hint | `null` | | `$exceptions->respond(fn ...)` | `bootstrap/app.php` | every exception response | must return a response | ## Render callbacks in bootstrap/app.php ```php $exceptions->render(function (SeatUnavailableException $e, Request $request) { return response()->view('booking.seat-taken', ['seat' => $e->seat], 409); }); ``` How the handler uses them: - The **first parameter's type-hint** selects the exceptions, including subclasses; a union type matches any of its classes. `$exceptions->renderable()` is an alias. - The callback receives the exception and the current `Request`. - Callbacks are tried in registration order, and the **first one to return a non-null value** wins. - Returning `null`, or nothing, means "not mine": the next matching callback is tried, then the default rendering. That is how you customise only some requests, for example only paths under `api/`. - Callbacks can also target framework and Symfony exceptions, such as `NotFoundHttpException`. ## A render() method on the exception A **renderable exception** carries its own presentation: ```php public function render(Request $request): Response|false ``` - It runs **before** any registered render callback. - Its return value is converted with the router's usual rules, so a response, a view, an array (sent as JSON) or a string all work. - A falsy return, typically `false`, hands the exception back to the normal path: callbacks, then defaults. That is useful when an exception extends a framework exception and only sometimes needs custom output. Exceptions that implement `Illuminate\Contracts\Support\Responsable` get similar treatment: the handler calls `toResponse($request)` on them after the `render()` check. ## respond(): the last word on every response `$exceptions->respond()` registers one callback that receives the **final** response, whichever path produced it: the exception's own `render()`, a render callback or the defaults. It is called with the response, the exception and the request, and whatever it returns is sent. Typical uses: 1. Add a header to every error response, such as `Cache-Control: no-store`. 2. Swap one status's default page for a redirect. 3. Normalise error bodies produced by third-party exceptions. Two details catch people out. The callback **must return a response**, because whatever it returns replaces the rendered one. And the handler stores a **single** callback, so a second `respond()` call replaces the first rather than chaining. ## The order in one list For each exception the handler: 1. applies any `map()` conversion; 2. calls the exception's own `render()` and uses a truthy result; 3. calls `toResponse()` on a `Responsable` exception; 4. converts some framework exceptions into HTTP exceptions, such as a missing model into a 404; 5. tries the render callbacks; 6. falls back to built-in rendering: error views, JSON, the login redirect for unauthenticated requests; 7. passes the result through `respond()`, if registered. ## Choosing between them - Put presentation on the **exception class** when the exception is yours and always renders the same way; the rule then travels with it and is easy to find. - Use a **render callback** for framework or third-party exceptions you cannot edit, and for rules that depend on the surface, such as API paths versus web pages. - Reserve **`respond()`** for cross-cutting adjustments to every error response; it is a poor place for per-exception logic because it only sees the finished response. - Whatever you choose, keep one place per exception type, so two teams do not write competing rules that silently shadow each other. ## Rendering is not reporting Rendering decides what the client sees; reporting decides what gets logged, and it runs separately. A render callback that returns a friendly 409 does not stop the exception being logged, and silencing the log does not change the response. ## Common mistakes - Returning a response from a callback typed on the wrong class, so it never matches. - Forgetting that `null` falls through, and expecting an empty response. - Registering two `respond()` callbacks and losing the first. - A `respond()` callback that modifies the response but forgets to return it. - Expecting a render callback to hide the exception from the logs.

  • What happens when a render callback returns null?
    The handler treats it as 'not handled' and tries the next matching render callback, then falls back to its default rendering. Returning `null` is how a callback customises only some requests.
  • If the exception has a render() method and a render callback also matches, which wins?
    The exception's own `render()` runs first; if it returns a truthy value, that response is used and the callbacks are never tried. Only a falsy return lets the callbacks run.
  • Can you register several respond() callbacks?
    No. The handler keeps a single response-finalising callback, so each `respond()` call replaces the previous one. Put all final-response adjustments in one callback.

saying these in an interview costs you the question

  • A render callback returning null sends an empty response
  • Render callbacks run before the exception's own render() method
  • Each respond() call adds another callback to a chain
  • A render callback also stops the exception from being logged
  • Render callbacks only work for your own exception classes