
A Rails transaction wraps multiple database writes into one atomic unit: ActiveRecord::Base.transaction do ... end. If any statement raises an exception, ActiveRecord rolls back every change; if the block finishes, everything commits. Use bang methods (save!, create!) so failures raise. Raising ActiveRecord::Rollback cancels the transaction silently — the block swallows it.
This guide is a reference for how transactions behave in current Rails: the syntax, the exact rollback rules, error handling that doesn’t fight the transaction machinery, and the transaction callbacks added in Rails 7.2. Every example is verified on Rails 8 (ActiveRecord 8.1) and Ruby 3.4.
Rails Transaction Syntax
There are three equivalent ways to open a transaction:
ActiveRecord::Base.transaction do
# ...
end
User.transaction do
# ...
end
user.transaction do
# ...
endAll three start the same database transaction. On a single-database app, the receiver changes only the readability. With multiple databases, it also picks the connection the transaction opens on — writes to a model on a different database run outside the transaction — so invoke transaction on the model whose database you are writing to. A useful convention:
ActiveRecord::Base.transactionwhen the block mixes several models and service calls:
ActiveRecord::Base.transaction do
attributes = user.prepare_attributes(account)
membership = Membership.create!(attributes)
LogService.log_creation(user, membership)
endModel.transactionwhen the block mostly touches that one model:
User.transaction do
user = User.create!(attributes)
user.log_activity("creation")
endinstance.transactionwhen everything operates on a model instance you already hold:
user.transaction do
user.charge!(amount)
user.log_activity("charge")
endHow Rollbacks Work in Rails Transactions
Whether a transaction commits or rolls back depends entirely on how control leaves the block:
| Inside the block | Transaction | What the caller sees |
|---|---|---|
| An exception is raised | Rolls back | The exception, re-raised after the rollback |
raise ActiveRecord::Rollback | Rolls back | Nothing — the block swallows it and returns nil |
return or break | Commits | Execution continues; earlier writes are committed |
| The block finishes | Commits | The block’s return value |
Silent Rollbacks with ActiveRecord::Rollback
ActiveRecord::Rollback is the one exception with special treatment. It rolls the transaction back, but the block swallows it, so nothing reaches the calling code.
ActiveRecord::Base.transaction do
order.update!(status: "paid")
raise ActiveRecord::Rollback if payment_declined?
end
# Execution continues here with the update rolled back —
# no exception was raised to the caller.Use it when canceling the transaction is a normal outcome rather than an error. Because it fails silently, the caller can’t tell a commit from a rollback. If the caller needs to know, raise a regular exception instead.
return and break Commit the Transaction
In current Rails, exiting a transaction block early with return, break, or throw commits the writes made so far:
def create_user
User.transaction do
user = User.create!(name: "Ada")
return if skip_membership? # the user above is still committed
Membership.create!(user: user)
end
endEarlier Rails versions rolled back on return and went through a deprecation cycle to reach today’s behavior. Older articles and answers often state the opposite. On Rails 8, an early return is a commit. If you want to cancel, raise ActiveRecord::Rollback or another exception.
Nested Transactions and requires_new
Calling transaction inside a transaction does not open a real second transaction by default. The inner block is fused into the outer one, which produces a classic gotcha: ActiveRecord::Rollback raised in the inner block is swallowed by the inner block and rolls back nothing.
ActiveRecord::Base.transaction do
User.create!(name: "Ada")
ActiveRecord::Base.transaction do
User.create!(name: "Grace")
raise ActiveRecord::Rollback
end
end
# Both users are committed — the inner rollback was silently ignored.To make the inner block independently cancelable, pass requires_new: true. ActiveRecord emulates the nested transaction with a database savepoint, and the inner rollback undoes only the inner writes:
ActiveRecord::Base.transaction do
User.create!(name: "Ada")
ActiveRecord::Base.transaction(requires_new: true) do
User.create!(name: "Grace")
raise ActiveRecord::Rollback
end
end
# "Ada" is committed; "Grace" is rolled back.Error Handling in Rails Transactions
Rollbacks are driven by exceptions, so error handling and transactions are tightly coupled. These rules keep them working together instead of against each other.
Use Bang Methods So Failures Raise
Non-bang methods like save and create report failure with a return value, not an exception. Inside a transaction, that silence is a data-integrity bug: nothing raises, so nothing rolls back.
ActiveRecord::Base.transaction do
user = User.create(user_attributes)
user.memberships.create(membership_attributes)
endIf the membership fails validation here, the transaction still commits, and you end up with a user without the membership record it was supposed to have.
The bang versions raise on failure (create! raises ActiveRecord::RecordInvalid when validations fail), and the exception rolls back every write in the block:
ActiveRecord::Base.transaction do
user = User.create!(user_attributes)
user.memberships.create!(membership_attributes)
endNow either both records exist or neither does.
Rescue Outside the Transaction Block
Where you put the rescue decides whether the rollback happens. Rescuing inside the block catches the exception before the transaction machinery sees it, so the transaction commits:
User.transaction do
user.charge!
user.grant_access!
rescue SomeError
# The exception never reaches the transaction — it COMMITS,
# including whatever charge! wrote before the failure.
endLet the exception propagate out of the block, and rescue around the transaction instead:
def charge_user
User.transaction do
user.charge!
user.grant_access!
end
rescue SomeError
# The transaction has already rolled back — handle the failure here.
endThis way you get both behaviors you want: the rollback and the rescue.
Do Not Rescue from ActiveRecord::StatementInvalid
ActiveRecord::StatementInvalid means the database itself rejected a query — a syntax error, a constraint violation, a lost connection. Swallowing it hides real infrastructure problems:
def perform_action(...)
User.transaction do
# ...
end
rescue ActiveRecord::StatementInvalid
# Don't do this — you've silenced a database-level failure.
endLet these errors surface to your error tracker. You should always know when queries fail at the database level.
Do Not Catch Generic Errors
Rescuing broad classes like StandardError around a transaction catches far more than the failure you had in mind, including errors raised by unrelated code inside the block. That silences real bugs and makes rollback behavior unpredictable. Rescue the specific error classes you expect the block to raise, and let everything else propagate.
Transaction Callbacks: after_commit and after_rollback
Side effects like emails and background jobs don’t roll back. If you enqueue a job inside a transaction and the transaction rolls back, the job still runs against data that no longer exists.
Since Rails 7.2, the transaction block yields the transaction object, and you can register callbacks on it directly:
ActiveRecord::Base.transaction do |transaction|
transaction.after_commit do
OrderMailer.confirmation(order).deliver_later
end
order.update!(status: "paid")
endThe after_commit callback runs only if the transaction commits; on rollback it’s discarded. There’s also after_rollback for the inverse: cleanup or logging that should happen only when the transaction fails. Keep side effects in these callbacks rather than in the transaction body, and rollbacks stay side-effect-free.
When Not to Use Transactions
Transactions guarantee atomicity across multiple writes. Used elsewhere, they add cost without adding safety:
- A single query doesn’t need one. Every individual statement is already atomic; wrapping one
save!in a transaction adds overhead and nothing else. - Keep non-database work out of the block. An external API call or slow computation inside a transaction holds the database connection and any row locks for its whole duration. Do the slow work first, then open the transaction for the writes.
The Costs of Transactions
Even a correct transaction isn’t free. While it’s open, it occupies a connection from the pool and holds locks on every row it has written. Other requests queue behind those locks, and under load, a few long-running transactions are enough to exhaust the pool or deadlock with each other. Transactions also add complexity (rollback paths, callback timing, nesting), so keep the code inside a block as small as possible.
Long-running transactions rarely fail loudly — they surface as slow queries and stuck connections first. AppSignal’s database monitoring shows which transactions hold locks longest, so you catch contention before users do. The slow query guide shows how to find the offenders in a Rails app.
Wrapping Up
Rails transactions are a small API with precise rules: exceptions roll back and re-raise, ActiveRecord::Rollback rolls back silently, return and break commit, and nested blocks need requires_new: true to roll back independently. Use bang methods so failures raise, rescue outside the block, and move side effects into after_commit.
With those rules in place, a transaction block does exactly what it promises: all of the writes, or none of them.
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- Does Rails roll back a transaction on exceptions?
- Yes. Any exception raised inside a transaction block rolls back every write made in the block, and the exception is re-raised after the rollback. The only exception ActiveRecord swallows is ActiveRecord::Rollback, which rolls back silently.
- What does ActiveRecord::Rollback do?
- Raising ActiveRecord::Rollback inside a transaction block rolls back the transaction without re-raising: the block swallows the error and returns nil. Code after the transaction keeps running as if nothing failed.
- How do I roll back a transaction in Rails?
- Raise an exception inside the transaction block. Use bang methods like save! and create! so failed writes raise automatically, or raise ActiveRecord::Rollback to cancel the transaction without an error reaching the caller.
- Do return and break roll back a Rails transaction?
- No. In current Rails, exiting a transaction block with return or break commits any writes already made in the block. Only exceptions trigger a rollback.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Paweł Dąbrowski
Our guest author Paweł is an open-source fan and growth seeker with over a decade of experience in writing for both human beings and computers. He connects the dots to create high-quality software and build valuable relations with people and businesses.
All articles by Paweł DąbrowskiBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


