I have been using return
a fair bit in transactions in Rails. In 7, these rollback the transaction and I use them as so.
They obviously require very careful writing and to remember that the return actually just doesn't jump out of the block, it cancels the transaction.
I prefer the fact that return
from 8 will throw an exception, but I've found that using Github::Result
around transactions as a pattern comes pretty close or is better (in some cases) already.
Consider this example code that creates and merges a pull request,
def create_and_merge!
return Result.new(ok?: false) unless create.ok?
upserted_pull_request =
@new_pull_request.update_or_insert!(create.value)
transaction do
upserted_pull_request.close! # close the PR
if merge.ok?
Result.new(ok?: true)
else
return Result.new(ok?: false, error: "Failed!")
end
end
end
def create
# creates a PR and returns a Result
end
def merge
# merges a PR and returns a Result
end
Result = Struct.new(:ok?, :error, :value, keyword_init: true)
One problem here is that we're using custom Result
objects which are not very chainable. But the other more shape-y problem is that we're having to check the output from merge
, return an ok-Result
or else cancel the transaction and then return a not-ok-Result
. This not only feels like excessive work but also the use of return
is unfortunate to essentially carry out a rollback + value type scenario.
With Github::Result
we can rewrite it much more cleanly,
def create_and_merge!
return GitHub::Result.new { raise CreateError } unless create.ok?
upserted_pull_request =
@new_pull_request.update_or_insert!(create.value!)
GitHub::Result.new do
transaction do
upserted_pull_request.close! # close the PR
merge.value!
end
end
end
As long as merge
throws an exception, value!
will raise it, rollback (throwing an exception will rollback) and opaquely pass it further up to the wrappper Result
. This allows us to avoid return
magic and ugly raises in the middle of the transaction block and chain the exceptions up.