skip to content

In Eloquent, what do find(), findOrFail(), first() and firstWhere() return when no matching row exists?

level: juniorimportance: must knowfreq 72%

answer

  1. null versus an exception
  2. find() adds where on the primary key
  3. ModelNotFoundException from the OrFail variants
  4. firstWhere = where()->first()
  5. array of ids returns a Collection

basics

~10 s

find(), first() and firstWhere() return null when nothing matches, while findOrFail() and firstOrFail() throw ModelNotFoundException. Choose the null-returning form when absence is normal and the OrFail form when absence is an error.

solid answer

~40 s

`find($id)` looks the row up by primary key and returns the model or `null`; given an array of ids it returns a Collection that simply lacks the missing ones. `first()` returns the first row of whatever query you built, or `null`, and `firstWhere('sku', $sku)` is shorthand for `where('sku', $sku)->first()`. The `OrFail` variants, `findOrFail()` and `firstOrFail()`, throw `ModelNotFoundException` instead, which in an HTTP request Laravel renders as a 404. So I use the null-returning forms when a missing row is an expected case, such as a new SKU in an import, and the throwing forms when the row must exist and the caller cannot continue without it. `findOr()` takes a fallback closure, and `sole()` also throws when more than one row matches.

code

php · 12 lines
php
<?php

use App\Models\Category;
use App\Models\Product;

$product = Product::firstWhere('sku', $row['sku']);

if ($product === null) {
    $product = new Product(['sku' => $row['sku']]);
}

$category = Category::findOrFail($row['category_id']);

go deeper

for a junior

Recall which lookups return null and which throw ModelNotFoundException, and that find() uses the primary key.

for a middle

Explain what each method adds to the query, how array ids change find() and findOrFail(), and when sole() is the stricter choice.

for a senior

Choose null or exception by what absence means in the code path, and keep exception-driven lookups out of loops where missing rows are routine.

for a principal

Set conventions for lookups across services so absence semantics are consistent between HTTP handlers, jobs and imports.

## The retrieval methods and their empty case Every Eloquent read starts from a **query builder** for the model, for example `Product::query()` or a static call such as `Product::where(...)`. The finishing method decides what you get back, and above all what you get when **no row matches**. That empty case is what interviewers probe, because it decides whether the calling code needs a null check or an exception handler. | Call | Match found | No match | |---|---|---| | `Product::find(42)` | the model | `null` | | `Product::findOrFail(42)` | the model | throws `ModelNotFoundException` | | `Product::where('sku', $sku)->first()` | the first model | `null` | | `Product::firstWhere('sku', $sku)` | the first model | `null` | | `Product::where('sku', $sku)->firstOrFail()` | the first model | throws `ModelNotFoundException` | | `Product::where('sku', $sku)->sole()` | the only model | throws `ModelNotFoundException`; more than one row throws `MultipleRecordsFoundException` | | `Product::findOr(42, fn () => ...)` | the model | the closure's return value | ## What each one actually runs - `find($id)` adds a `where` on the model's primary key and calls `first()`. Passing an **array** of ids switches to `findMany()`, which returns a **Collection**; missing ids are simply absent from it. - `findOrFail($id)` calls `find()` and throws when the result is `null`. With an array of ids it throws if **any** id is missing, comparing the result count with the unique ids requested. - `first()` adds `limit 1` to whatever query you built and returns the first model or `null`. - `firstWhere('sku', $sku)` is shorthand for `where('sku', $sku)->first()`; it accepts the same column, operator and value arguments as `where()`. - `sole()` fetches at most two rows so it can tell "none" from "more than one", which makes it useful when a lookup must be unique. ## Choosing between null and an exception The rule of thumb is to match the method to what an empty result **means**: 1. **Missing is normal** — use `find()` / `first()` / `firstWhere()` and branch on `null`. In a nightly electronics-feed import, a SKU that is not in the catalogue yet is expected, so the importer creates it. 2. **Missing is an error the caller cannot fix** — use `findOrFail()` / `firstOrFail()`. The exception stops the code path cleanly; in an HTTP request, Laravel's exception handler turns an uncaught `ModelNotFoundException` into a 404 response. 3. **Missing needs a custom fallback** — use `findOr()` with a closure, which runs only when nothing is found. 4. **More than one row would be a data bug** — use `sole()` so the bug surfaces instead of silently picking one row. ## A feed-import example ```php $product = Product::firstWhere('sku', $row['sku']); if ($product === null) { // new SKU in tonight's feed } $category = Category::findOrFail($row['category_id']); ``` The first lookup treats absence as ordinary; the second treats an unknown category id as a broken feed row that should abort the item. ## Common mistakes - Chaining a method on the result of `find()` without a null check, which fails with "call to a member function on null" as soon as an id is missing. - Wrapping `findOrFail()` in `try/catch` just to return `null`, which rebuilds `find()` with extra steps. - Writing `Product::where('id', $id)->first()` where `find($id)` states the intent and respects a custom primary key name. - Expecting `find([1, 2, 3])` to return `null` for a missing id; it returns a Collection without that id. - Using `first()` on a query that should match exactly one row, which hides duplicates that `sole()` would report. ## The rest of the family The same null-or-throw pattern repeats across a few less common helpers, and naming them shows you know the API rather than two methods of it: - `findMany([...])` is the explicit form of `find()` with an array and always returns a Collection, empty when nothing matches. - `findOrNew($id)` returns the found model or a **new, unsaved** instance, never `null`. - `findSole($id)` is `sole()` scoped to one key, throwing for none or several. - `firstOr(fn () => ...)` is `findOr()` for an arbitrary query: the closure runs only when `first()` finds nothing. None of these adds a new query shape; each is a thin wrapper that decides what an empty result becomes.

  • What does `Product::findOrFail([1, 2, 3])` do if only ids 1 and 3 exist?
    It throws `ModelNotFoundException`. With an array, `findOrFail()` compares the number of models returned with the number of unique ids requested and throws when they differ, recording the missing ids on the exception. Plain `find([1, 2, 3])` would instead return a Collection holding the two models it found.
  • When would you use `sole()` instead of `first()` on an Eloquent query?
    When exactly one row must match and a second one would be a data bug, for example looking up a product by a supplier code that should be unique. `sole()` fetches up to two rows and throws `ModelNotFoundException` for none or `MultipleRecordsFoundException` for more than one, while `first()` would silently return whichever row came first.

saying these in an interview costs you the question

  • find() throws an exception when the id does not exist
  • firstWhere() throws when no row matches
  • find([1, 2, 3]) returns null if one of the ids is missing
  • findOrFail() returns false when nothing is found
  • Catching ModelNotFoundException to return null is the idiomatic lookup