skip to content

In Laravel, how do withTrashed(), missing() and a backed-enum route parameter change what a bound route does when the lookup fails or finds a soft-deleted row?

level: middleimportance: should knowfreq 30%

answer

  1. SoftDeletes rows 404 by default
  2. withTrashed() on the route definition
  3. missing(fn ($request, $e) => ...)
  4. missing() only catches ModelNotFoundException
  5. enum: tryFrom, else 404

basics

~20 s

Soft-deleted models 404 unless the route calls withTrashed(). missing() swaps the not-found 404 for your own response, such as a redirect. A backed-enum parameter binds only valid cases; anything else is a 404 that missing() does not intercept.

solid answer

~40 s

A model using `SoftDeletes` is bound through its global scope, so a trashed task is "not found" and the route answers 404. `->withTrashed()` on the route switches binding to a query that includes trashed rows, for every soft-deletable model bound on that route — handy for a restore endpoint. `->missing(function (Request $request, ModelNotFoundException $e) { ... })` catches the `ModelNotFoundException` thrown by implicit or explicit binding and returns your response instead, such as a redirect back to the project's task list. A parameter type-hinted with a backed enum is bound via `tryFrom()`; an invalid value throws `BackedEnumCaseNotFoundException`, which the exception handler renders as a 404 — and `missing()` does not catch it, because it only handles `ModelNotFoundException`.

code

php · 13 lines
php
<?php

use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;

Route::patch('/tasks/{task}/restore', [TaskController::class, 'restore'])
    ->withTrashed();

// TaskController::restore(Task $task)
// {
//     $task->restore();
//     return back();
// }

go deeper

for a junior

Recall that a soft-deleted model 404s unless the route calls withTrashed(), and that an invalid enum segment is also a 404.

for a middle

Explain how withTrashed() swaps the binding query, what the missing() closure receives, and why missing() ignores enum failures.

for a senior

Scope withTrashed() to the routes that need it, keep APIs on honest 404s, and authorize access to archived records.

for a principal

Define how deleted and archived records surface in URLs — 404, redirect or archive page — consistently across web and API clients.

## Three ways a bound route can "not find" something In a project tracker, the task routes meet three kinds of failed or unusual lookups: 1. The task was **soft-deleted** (archived) and someone opens its old URL or wants to restore it. 2. The task **never existed** or was hard-deleted. 3. A filter segment such as `/tasks/status/{status}` holds a value that is **not a valid status**. Laravel's route model binding handles each one differently. ## Soft-deleted models and `withTrashed()` A model that uses the `SoftDeletes` trait has a global scope adding `deleted_at is null` to its queries. Binding goes through that scope, so a trashed task behaves exactly like a missing one: **404**. Calling `->withTrashed()` on the route changes that: - Binding switches to `resolveSoftDeletableRouteBinding()`, which runs the same query with `withTrashed()`. - It applies to **every soft-deletable model bound on that route**, including a scoped child and models bound with `Route::model()`. - Models without `SoftDeletes` are unaffected. Typical uses are a restore endpoint (`PATCH /tasks/{task}/restore`) or an "archived task" page. Because the route now exposes deleted records, check authorization and render trashed items clearly. ## Custom not-found behaviour with `missing()` By default, a failed model binding throws `ModelNotFoundException`, rendered as a 404. The `missing()` method on a route definition lets you respond differently: ```php Route::get('/projects/{project}/tasks/{task}', [TaskController::class, 'show']) ->scopeBindings() ->missing(function (Request $request) { return redirect()->route('projects.index') ->with('status', 'That task no longer exists.'); }); ``` - The closure receives the **request** and the **`ModelNotFoundException`**. - It runs for failures from both explicit binders and implicit binding on that route. - Whatever it returns is sent instead of the 404; the action never runs. - Use it for user-friendly flows — a redirect with a flash message after a deleted task link — not to hide missing records from APIs that should report a 404. ## Backed-enum parameters A route action can type-hint a **backed enum**: ```php enum TaskStatus: string { case Open = 'open'; case Done = 'done'; } Route::get('/tasks/status/{status}', fn (TaskStatus $status) => $status->value); ``` - Laravel converts the segment with `TaskStatus::tryFrom($value)` and injects the case. - An invalid value throws `BackedEnumCaseNotFoundException`, which the exception handler maps to a **404**. - The docs describe string-backed enums; they are the natural fit for URL segments. - `missing()` does **not** run for an enum failure, because it catches only `ModelNotFoundException`. ## Summary | Situation | Default outcome | How to change it | |---|---|---| | Soft-deleted model | 404 | `->withTrashed()` on the route | | Missing model | 404 via `ModelNotFoundException` | `->missing(fn ($request, $e) => ...)` | | Invalid enum value | 404 via `BackedEnumCaseNotFoundException` | Customise exception rendering; `missing()` is not involved | ## Order of evaluation Inside the binding middleware the steps run in a fixed order, which explains most surprises: 1. **Explicit binders** registered with `Route::model()` or `Route::bind()` run first, parameter by parameter. 2. **Backed-enum parameters** are converted next; an invalid value throws immediately. 3. **Implicit model bindings** run last, left to right, each child scoped by its parent when scoping is on. Only a `ModelNotFoundException` thrown in steps 1 or 3 is routed to the `missing()` closure; everything else goes to the exception handler. Because the steps run before the action, a feature test that requests a trashed task, a nonexistent task and an invalid status should see three predictable outcomes without any controller code executing. ## Pitfalls - Adding `withTrashed()` to a whole group because one route needs it, and exposing archived records on every route in it. - Using `missing()` to redirect API clients, who then receive a 302 and HTML instead of a 404. - Expecting `missing()` to handle a bad enum value and shipping a raw 404 page for it.

  • A Laravel route type-hints TaskStatus $status and has ->missing(...). Why does /tasks/status/bogus still show the plain 404?
    Enum binding throws `BackedEnumCaseNotFoundException`, while the binding middleware only hands `ModelNotFoundException` to the `missing()` closure. The enum exception propagates to the exception handler, which renders it as a 404. Customise that exception's rendering if you need a different response.
  • Does withTrashed() on a scoped route also let a soft-deleted child task bind?
    Yes. When the route allows trashed bindings, Laravel uses the soft-deletable variants for both top-level and scoped child lookups, so a trashed task under its project binds instead of returning a 404, as long as the task model uses `SoftDeletes`.

saying these in an interview costs you the question

  • Soft-deleted models are bound like any other row
  • missing() also handles invalid enum values
  • withTrashed() makes the model include trashed rows in every query
  • A failed binding returns 410 Gone for soft-deleted rows
  • missing() receives only the missing segment value