skip to content

In Active Record, what do find, find_by and find_by! do when no matching row exists, and when do you choose each?

level: juniorimportance: should knowfreq 68%

answer

  1. primary key versus any conditions
  2. find raises RecordNotFound
  3. find_by returns nil
  4. find_by! raises like find
  5. find with an array needs every id

basics

~10 s

find looks up by primary key and raises ActiveRecord::RecordNotFound when it is missing; find_by takes any conditions and returns nil; find_by! takes conditions but raises RecordNotFound like find.

solid answer

~40 s

`Listing.find(42)` queries by primary key and raises `ActiveRecord::RecordNotFound` ("Couldn't find Listing with 'id'=42") if no row exists; `find([42, 43])` raises unless **every** id is found. `Listing.find_by(slug: "road-bike-54")` accepts any conditions, runs the query with `LIMIT 1` and returns the first match or `nil`. `find_by!` behaves like `find_by` but raises `RecordNotFound` instead of returning `nil`. Choose by what a miss means: when the record must exist (a URL with an id or slug), use `find` or `find_by!` and let the exception surface; when absence is a normal case ("has this seller already listed this item?"), use `find_by` and branch on `nil`. Calling a method on a `find_by` result without a nil check is the classic `NoMethodError` on `nil`.

code

ruby · 8 lines
ruby
Listing.find(42)
# => ActiveRecord::RecordNotFound: Couldn't find Listing with 'id'=42

Listing.find_by(slug: "no-such-slug")   # => nil
Listing.find_by!(slug: "no-such-slug")  # => ActiveRecord::RecordNotFound

# Scoped: someone else's listing is "not found" for this user
Current.user.listings.find(params[:id])

go deeper

for a junior

Recall that find raises RecordNotFound, find_by returns nil and find_by! raises, and that find searches only the primary key.

for a middle

Explain how find with several ids fails, why find_by needs a nil check, and when find_by! beats find.

for a senior

Scope lookups through the owning association so a foreign record is simply not found, instead of loading it and checking ownership.

for a principal

Decide how a codebase treats misses consistently, raising for addressed resources and returning nil only where absence is a business case.

## Three lookups, two failure styles Active Record's single-record lookups differ in **what they search on** and **what a miss does**: | Call | Searches on | No match | |---|---|---| | `Listing.find(42)` | the primary key | raises `ActiveRecord::RecordNotFound` | | `Listing.find([42, 43])` | several primary keys | raises unless all are found | | `Listing.find_by(slug: "road-bike-54")` | any conditions | returns `nil` | | `Listing.find_by!(slug: "road-bike-54")` | any conditions | raises `ActiveRecord::RecordNotFound` | ## find `find` takes primary key values. With one value it returns a record or raises `RecordNotFound` with a message such as `Couldn't find Listing with 'id'=42`. With an array (or several arguments) it returns an array, and raises if **any** id is missing, reporting how many it found against how many it looked for. `find(nil)` raises too (`Couldn't find Listing without an ID`). Because it raises, `find` is the natural call for a resource identified by the URL: a missing record is a client error, and the exception is how Rails turns it into a not-found response (that mapping belongs to controller error handling). ## find_by `find_by(conditions)` is shorthand for `where(conditions)` taking one row: the query carries `LIMIT 1` and returns the first match or `nil`. It accepts the same arguments as `where`, including several attributes and a SQL fragment with bind values: ```ruby Listing.find_by(seller_id: seller.id, title: "Road bike, 54 cm") Listing.find_by("price_cents < ?", 10_000) ``` Two cautions: - **No ordering is implied.** If several rows match, which one comes back is up to the database. Add an `order` or make the conditions unique. - **`nil` must be handled.** `Listing.find_by(slug: params[:slug]).title` raises `NoMethodError` on `nil` when the slug is wrong, which is a worse error than `RecordNotFound`. ## find_by! `find_by!` is `find_by` that raises `ActiveRecord::RecordNotFound` on a miss. It is the right call when a resource is addressed by something other than its id, such as a slug, a token or a seller-scoped number: you get conditions **and** the not-found behaviour of `find`. ## Dynamic finders Active Record also answers `find_by_slug("road-bike-54")` and `find_by_slug!(...)`, generated from attribute names. They behave like `find_by(slug: ...)` and `find_by!(slug: ...)`; the hash form is more common in current code because it combines several conditions naturally. ## Choosing 1. The record **must** exist and you have its id: `find`. 2. The record must exist and you have another unique key: `find_by!`. 3. Absence is a normal outcome you branch on: `find_by`, then check for `nil`. 4. You need to know only whether something exists, not load it: an existence query is cheaper than either (that belongs to querying). ## Scoping lookups All three work on relations and associations: `Current.user.listings.find(params[:id])` raises `RecordNotFound` when the listing exists but belongs to someone else, which is both a correctness and an authorization guard. Calling `Listing.find(params[:id])` and checking ownership afterwards is the weaker pattern. ## The SQL behind each call The queries are nearly identical; only the failure handling differs: ```sql -- Listing.find(42) SELECT "listings".* FROM "listings" WHERE "listings"."id" = 42 LIMIT 1 -- Listing.find([42, 43]) SELECT "listings".* FROM "listings" WHERE "listings"."id" IN (42, 43) -- Listing.find_by(slug: "road-bike-54") and find_by! SELECT "listings".* FROM "listings" WHERE "listings"."slug" = 'road-bike-54' LIMIT 1 ``` So the performance is the same; pick by what a miss should do. Note that `find_by` with a column that is not unique silently returns one arbitrary match, which is why addressing records by slug works only when the slug column carries a unique index.

  • What does Listing.find([3, 7, 9]) do when listing 7 does not exist?
    It raises `ActiveRecord::RecordNotFound`. With several ids, `find` requires all of them; the message reports how many rows it found against how many it expected. To get whichever exist, use `Listing.where(id: [3, 7, 9])`, which returns the found rows without raising.
  • Why is Current.user.listings.find(params[:id]) better than Listing.find(params[:id]) followed by an owner check?
    The scoped call adds `user_id = ?` to the query, so another user's listing simply is not found and raises `RecordNotFound`. There is no window where the wrong record is loaded, and no check to forget in one action.

saying these in an interview costs you the question

  • Saying find_by raises RecordNotFound when no row matches.
  • Expecting find([3, 7, 9]) to return only the listings that exist.
  • Calling a method on a find_by result without handling nil.
  • Using find with a slug value, expecting it to search the slug column.
  • Believing find_by orders by primary key before taking a row.