I faced the same issue today.
Mostly, I use Ecto.Multi to do transactions, and it has been very satisfactory to me.
Today I had to encapsulate some code on a Repo.transaction/2 without breaking the interface, which was {:ok, result} | {:error, changeset}.
Repo.transaction fn ->
with {:ok, alice} <- Person.add_amount(alice, -10),
{:ok, bob} <- Person.add_amount(bob, +10) do
Ledger.update_balance([alice, bob])
end
end
where
- Person.add_amount/2 :: {:ok, result} | {:error, reason}
- Ledger.update_balance/2 :: {:ok, result} | {:error, reason}
I was hoping that on Repo.transaction/2, when it receives a tuple {:error, reason}, it would rollback the transaction and return {:error, reason}, but instead it returned {:ok, {:error, reason}}, then I realized I'd have to do like what Nils suggested above:
Repo.transaction fn ->
with {:ok, alice} <- Person.add_amount(alice, -10),
{:ok, bob} <- Person.add_amount(bob, +10) do
Ledger.update_balance([alice, bob])
So, I'd suggest that Repo.transaction/2 could consider when the given function returns the conventioned tuples {:ok, result} | {:error, reason} to really bubble up these and commit or rollback based on them. :)