Model::findOrFail
Looks up a model by primary key and throws ModelNotFoundException when no row matches.
public function findOrFail(mixed $id, array|string $columns = ['*']): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\CollectionParameters
| Parameter | Type | Description |
|---|---|---|
$id | mixed | The primary key value to look up. An array or Arrayable of keys requires every key to be found. |
$columnsoptional | array|string= ['*'] | Columns to select on the hydrated model. |
Return value
\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\CollectionThe matching model. When $id is an array or Arrayable, an Eloquent Collection containing every requested key is returned. The method never returns null.
Errors
Throws Illuminate\Database\Eloquent\ModelNotFoundException when the key is not found — or, in array mode, when any requested key is missing. Uncaught, Laravel renders it as a 404 HTTP response.
findOrFail() is find() with the null branch removed. On a hit it returns the model; on a miss it throws Illuminate\Database\Eloquent\ModelNotFoundException. Laravel's exception handler recognises that class and, if you never catch it, converts it into a 404 HTTP response automatically.
When to reach for it
Use it anywhere a missing record means the request is over: controller actions resolving a route parameter, API endpoints, form handlers acting on an id the client supplied. It removes the guard clause and the manual abort(404) from the top of nearly every controller method.
Avoid it where a miss is a legitimate branch — a cache warm-up, an idempotent importer, an optional lookup. Throwing to control ordinary flow is expensive and hides the intent. Use find() or findOr() there instead.
What trips people up
The array form is stricter than people expect. findOrFail([1, 2, 3]) throws unless every distinct id is found — the implementation compares the result count against array_unique($id). A partial match is a failure, not a shorter collection.
Outside an HTTP request — in a queued job, a console command, a test — nothing converts the exception into a 404. It propagates as a normal exception, failing the job or the command.
Example
<?php
use App\Models\Flight;
use Illuminate\Database\Eloquent\ModelNotFoundException;
// Uncaught, this renders a 404 response.
Route::get('/api/flights/{id}', function (string $id) {
return Flight::findOrFail($id);
});
// Caught, it is a normal exception you can inspect.
try {
$flight = Flight::findOrFail(999999);
} catch (ModelNotFoundException $e) {
echo $e->getModel(), "\n"; // App\Models\Flight
print_r($e->getIds()); // Array ( [0] => 999999 )
}When to use it
- Resolve a route parameter in a controller so a bad id renders a 404 without a manual guard.
- Load the record a form submission targets before applying an update.
- Assert in a queued job that a referenced model still exists, failing the job loudly if it was deleted.
Frequently asked questions
How does findOrFail() turn into a 404?
Laravel's exception handler has a dedicated branch for ModelNotFoundException that converts it into a NotFoundHttpException, which renders as a 404 response. This only happens for exceptions that escape during an HTTP request — catching the exception yourself, or throwing it inside a queued job or console command, bypasses that translation entirely.
What is the difference between findOrFail() and firstOrFail()?
findOrFail() looks a model up by its primary key, while firstOrFail() takes the first row matching whatever constraints you already chained. Both throw ModelNotFoundException on a miss and both render as a 404. Use findOrFail($id) for an identifier and Model::where(...)->firstOrFail() for any other condition.
Why does findOrFail() throw when some of my ids exist?
Because the array form requires every distinct id to be found — it compares the returned count against array_unique() of the ids you passed, and throws if any are missing. The exception's getIds() lists exactly which ones were absent. If a partial match is acceptable, use find() with an array, which returns a possibly shorter collection instead.
More examples
Replacing the manual 404 guard
Both actions render the same page for a valid id and the same 404 for a bad one. The second shows why findOrFail() exists: the framework's handler already knows how to turn ModelNotFoundException into a 404, so the explicit abort() is redundant.
<?php
use App\Models\Post;
class PostController extends Controller
{
// Before: three lines of ceremony in every action.
public function showVerbose(string $id)
{
$post = Post::find($id);
if (! $post) {
abort(404);
}
return view('posts.show', ['post' => $post]);
}
// After: identical behaviour, one line.
public function show(string $id)
{
return view('posts.show', [
'post' => Post::findOrFail($id),
]);
}
}The strict array form
The first count shows find() quietly dropping the missing id. The exception's getIds() names exactly which id was absent, which is what makes the array form useful for validating a batch. The last line shows duplicates are de-duplicated before the count comparison, so repeating an id does not cause a false failure.
<?php
use App\Models\Product;
use Illuminate\Database\Eloquent\ModelNotFoundException;
// Suppose products 1 and 2 exist, but 99 does not.
// find() is lenient - returns the two that matched.
$found = Product::find([1, 2, 99]);
echo $found->count(), "\n"; // 2
// findOrFail() is all-or-nothing.
try {
Product::findOrFail([1, 2, 99]);
} catch (ModelNotFoundException $e) {
// The ids come from array_diff(), which preserves the original keys.
print_r($e->getIds()); // Array ( [2] => 99 )
}
// Duplicates are ignored - this succeeds, since only 1 and 2 are required.
echo Product::findOrFail([1, 1, 2])->count(), "\n"; // 2Outside an HTTP request
The job prints the plan name on a hit. On a miss the exception is never translated to a 404 because there is no request in flight — it bubbles up, the job is marked failed, and failed() receives the ModelNotFoundException so you can distinguish a deleted record from a genuine error.
<?php
use App\Models\Subscription;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class ChargeSubscription implements ShouldQueue
{
public function __construct(public int $subscriptionId) {}
public function handle(): void
{
// No HTTP layer here - a miss fails the job, it does not 404.
$subscription = Subscription::findOrFail($this->subscriptionId);
echo $subscription->plan, "\n";
}
// Decide deliberately whether a deleted subscription is an error.
public function failed(Throwable $e): void
{
if ($e instanceof ModelNotFoundException) {
echo "subscription {$this->subscriptionId} was deleted\n";
}
}
}
Discussion