‹ Back to Blog

Grape's error! Inside a Transaction Commits Your Data. Grape 4.1 Fixes It.

Engineering Ruby

If your Grape API calls error! inside an Active Record transaction, the client gets an error response and the database keeps the writes. No exception, no warning, no rollback. The request looks failed from the outside and succeeded on the inside.

A fix merged into Grape on October 3 and is queued for Grape 4.1.0. It’s not released yet, so every current Grape version still behaves this way. Here’s why it happens, how to check whether you’re affected, and what changes when 4.1 ships.

Why error! Skips the Rollback

Until now, Grape’s error! didn’t raise an exception. It used Ruby’s throw :error, which jumps out of the endpoint to a matching catch further up the stack.

throw isn’t an exception, and that matters to Active Record. A transaction rolls back only when an exception leaves its block. Any other exit, including return, break, and throw, commits it.

So this endpoint saves the order and then tells the client it failed:

post '/orders' do
  ActiveRecord::Base.transaction do
    order = Order.create!(declared(params))
    error!('Out of stock', 409) unless Inventory.reserve(order)
  end
end

The client sees a 409. The orders table has a new row.

Rails used to warn about this. Back in 2021, Grape issue #2197 reported Rails 6.1’s deprecation warning about using throw to exit a transaction block, triggered by error!. The warning is gone now. The behavior isn’t: according to the fix’s PR, since Rails 7.2 the commit happens with no warning at all. The PR reproduces it on Grape 4.0.1 with Active Record 8.1.4: the request answers 409 and leaves the row behind.

There’s a second problem with throw on fiber-based servers. A throw can’t leave the fiber it was thrown in, so on Falcon an error! inside an Async block became an UncaughtThrowError and a 500 instead of the status you asked for.

Are You Affected?

Search your API code for error! calls inside transaction blocks:

grep -rn -B8 "error!" app/api | grep -n "transaction"

That’s a rough first pass. Also check helpers and service objects that call error! and get invoked from inside a transaction. The call doesn’t have to be in the same file to cause this.

You can also look for it in production. A request that returns a 4xx but ran an INSERT or UPDATE is a strong hint. Any APM that shows the SQL for each request will surface it. In Scout, open the trace for a 4xx response on a write endpoint and check whether it contains writes that should have rolled back.

The Workaround Until 4.1

Make the transaction roll back explicitly, then call error! after the block:

post '/orders' do
  out_of_stock = false

  ActiveRecord::Base.transaction do
    order = Order.create!(declared(params))
    unless Inventory.reserve(order)
      out_of_stock = true
      raise ActiveRecord::Rollback
    end
  end

  error!('Out of stock', 409) if out_of_stock
end

ActiveRecord::Rollback is the exception Active Record expects for “undo this and carry on”. It’s swallowed by the transaction block, so execution continues and error! runs outside it. Raising any other exception inside the block also rolls back, but you’d then need to rescue it yourself.

What Changes in Grape 4.1

In 4.1, error! raises Grape::Exceptions::Halt instead of throwing. An exception leaving the transaction block triggers the rollback, so the original endpoint above does the right thing without changes. It also fixes the Falcon case.

Halt was designed with a few constraints, according to the PR:

  • It’s a StandardError. The async gem treats other exceptions leaving a task as fatal, which would take down every in-flight request on the worker.
  • It isn’t a Grape::Exceptions::Base. So it never reaches your rescue_from :all or rescue_from StandardError handlers, which would otherwise replace the status you passed to error!.
  • It skips capturing a backtrace. That keeps raising cheap. Combined with a lighter ErrorResponse object, the PR’s benchmarks show error! responses getting 7 to 23% faster, not slower.

Check these before you upgrade

The change is breaking in a few specific places, all listed in Grape’s UPGRADING notes:

  • rescue => e or rescue StandardError around error! now catches it. If you wrap code that calls error! in a broad rescue, that rescue will swallow the error response. Search for broad rescues near error! calls.
  • catch(:error) around error! gets the exception, not the payload. Rare in app code, but check any custom middleware.
  • Instrumentation payloads now include the exception. The endpoint_run.grape, endpoint_render.grape, and endpoint_run_filters.grape notifications carry :exception and :exception_object when error! ends a request. If you subscribe to these yourself, an error! that returns a 404 may now look like a crash. Filter on Grape::Exceptions::Halt.
  • Grape::Exceptions::ErrorResponse is no longer a Data object. It loses #with, value equality, #to_h, and pattern matching. Only matters if you build or inspect these directly.

That third point applies to monitoring tools too. Anything that watches Grape’s notifications or wraps endpoint execution needs to tell a deliberate error! apart from an unhandled exception, or your error rate will jump on upgrade day. If you use an APM with Grape, check its release notes when you upgrade.

throw :error from your own middleware and rescue_from handlers keeps working, so you don’t need to rewrite those.

The Takeaway

Today: find error! calls that run inside transactions and move them out, using ActiveRecord::Rollback to undo the writes. When Grape 4.1 ships: upgrade, then audit broad rescue blocks and any notification subscribers.

This bug is a good example of why a status code isn’t the whole story. A request that returns 409 and commits a row looks fine on a dashboard that only counts errors. Seeing the SQL each request actually ran is what catches it. Scout Monitoring shows that for every Grape and Rails endpoint, alongside N+1 detection, error tracking, and background job timing.

Try Scout Monitoring free. No credit card needed.

For application monitoring with errors, logs, and traces, Scout Monitoring provides the fastest insights without the bloat.