In Active Record, what do save, create and update return when a record is invalid, and how do save!, create! and update! differ?
answer
- boolean versus exception
- save returns false, errors filled
- create returns the unsaved object
- RecordInvalid carries the record
- RecordNotSaved when a callback aborts
basics
~10 ssave and update return false and fill errors; create returns the object unsaved, with errors. The bang versions raise ActiveRecord::RecordInvalid for failed validations and ActiveRecord::RecordNotSaved when a before callback aborts.
solid answer
~40 sThe non-bang methods report failure as a value. `listing.save` returns `true` or `false`; on `false`, `listing.errors` says why. `listing.update(price_cents: -5)` assigns, saves inside a transaction and returns the same boolean. `Listing.create(title: "")` always returns the object, so you check `persisted?` or `errors`. The bang methods raise instead: `save!`, `update!` and `create!` raise `ActiveRecord::RecordInvalid` (with `e.record.errors`) when validations fail, and `ActiveRecord::RecordNotSaved` when a `before_*` callback throws `:abort`. Use the boolean form where failure is an expected branch, like a controller re-rendering a form; use the bang form in jobs, seeds, scripts and inside transactions, where a silent `false` would let bad state slip through or keep a transaction from rolling back.
code
ruby · 11 lines# ListingsController#create
@listing = Current.user.listings.new(listing_params)
if @listing.save
redirect_to @listing, notice: "Listing published."
else
render :new, status: :unprocessable_content
end
# A seed or job: fail loudly
Listing.create!(title: "Road bike, 54 cm", price_cents: 32_000)
# => ActiveRecord::RecordInvalid if a validation failsgo deeper
Recall the pairs: save, update and create report failure as false or an unsaved object, while save!, update! and create! raise.
Explain RecordInvalid versus RecordNotSaved, why create's return value is always truthy, and which failures neither form catches.
Choose the form by context: booleans in controllers, bang methods in jobs, seeds and transaction blocks where silent false loses data.
Set a team convention for write failures so services, jobs and controllers surface them consistently rather than mixing silent falses with exceptions.
## Two ways to report the same failure Active Record writes come in pairs. Each pair runs the same validations and callbacks and sends the same SQL when it succeeds. They differ only in **how failure reaches the caller**: | Call | Success | Validation fails | `before_*` callback throws `:abort` | |---|---|---|---| | `record.save` | `true` | `false`, `errors` filled | `false` | | `record.save!` | `true` | raises `ActiveRecord::RecordInvalid` | raises `ActiveRecord::RecordNotSaved` | | `record.update(attrs)` | `true` | `false` | `false` | | `record.update!(attrs)` | `true` | raises `RecordInvalid` | raises `RecordNotSaved` | | `Model.create(attrs)` | the saved object | the **unsaved** object | the unsaved object | | `Model.create!(attrs)` | the saved object | raises `RecordInvalid` | raises `RecordNotSaved` | ## new + save versus create `Listing.new(attrs)` builds an object in memory; nothing touches the database until `save`. `Listing.create(attrs)` is literally `new` followed by `save`, returning the object either way. That is why this check is a bug: ```ruby if Listing.create(listing_params) # always truthy: an object is returned ``` The correct checks are `listing.persisted?` or `listing.errors.any?` after `create`, or, more commonly, `new` plus `if @listing.save` in the controller. ## update is assign plus save, in a transaction `listing.update(price_cents: 4500)` assigns the attributes and calls `save` inside a transaction, so any side effects of the assignment roll back if the save fails. The return value is `save`'s boolean. `update!` does the same with `save!`. ## What the exceptions carry - `ActiveRecord::RecordInvalid` exposes the failed object as `e.record`, so `e.record.errors.full_messages` gives the reasons. - `ActiveRecord::RecordNotSaved` means validations passed (or were skipped) but a `before_*` callback stopped the save. - Database-level failures (a `NOT NULL` violation, a unique index) are **not** caught by either form: `save` rescues only `RecordInvalid`, so those exceptions propagate from both `save` and `save!`. ## Choosing between them 1. **Controllers handling user input**: `if @listing.save ... else render :new, status: :unprocessable_content end`. Invalid input is a normal outcome, so a boolean branch reads best. 2. **Jobs, seeds, rake tasks, console scripts**: use `create!` / `update!`. A `false` nobody checks silently drops data; an exception fails loudly and lands in the job's error handling. 3. **Inside a transaction block**: use bang methods. A transaction rolls back when an exception escapes the block; a `save` returning `false` raises nothing, so the other writes in the block commit. 4. **Tests and fixtures setup**: bang methods, so a broken factory or fixture fails at the line that caused it. ## Skipping validations Both `save(validate: false)` and `save!(validate: false)` skip validations but still run callbacks and still set timestamps. It is an escape hatch for data repairs, not for normal flows. ## Common misreadings - **"`save` returns `false` whenever the row is not written."** Database rejections still raise; only failed validations and aborted callbacks become `false`. - **"`create` returns `false` when invalid."** It returns the object; only `save` and `update` return booleans. - **"The bang just means the method is faster or dangerous."** In Active Record the bang means "raise instead of returning a failure value". ## What the bang does not change Candidates sometimes describe the bang methods as a different save. They are not: - the **same validations** run, in the same order; - the **same callbacks** run, and `throw :abort` stops both; - the **same SQL** is sent when the save succeeds; - the **same timestamps** are written; - `update!`, like `update`, wraps assignment and save in a transaction. The only difference is the failure channel: a value the caller must check, or an exception the caller must rescue (or let propagate). That is why the choice is about **who is listening**: a controller branch that renders the form again, or a job runner that records a failure and retries. A good answer names both exception classes and says which failures neither form converts, because database errors raise from both.
- Inside ActiveRecord::Base.transaction, a listing.save returns false. Does the transaction roll back?No. A transaction block rolls back when an exception escapes it; `save` returning `false` raises nothing, so the block carries on and earlier writes commit. Use `save!` (or raise `ActiveRecord::Rollback`) to make the failure undo the block.
- A before_save callback throws :abort. What do save and save! do?`save` returns `false` and `save!` raises `ActiveRecord::RecordNotSaved`. Validations may have passed, so `errors` can be empty; the callback, not a validator, stopped the write.
saying these in an interview costs you the question
- Writing if Listing.create(params) and expecting the branch to catch invalid input.
- Saying save returns false when the database rejects a NOT NULL or unique-index violation.
- Using save without checking its result inside a transaction meant to roll back.
- Claiming the bang methods skip validations to save faster.
- Expecting update! to raise RecordNotFound when validations fail.