madili
why transaction/2 raises an exception instead of a return tuple
Hi,
Looking at the documentation of the transaction/2 function it should return a {:ok, result} or {:error, reason} tuple. However, if I use insert_all/3 with transaction/2 I don’t get the expected result, but an exception is thrown anyway.
For example: if I try to insert a duplicate record an exception is thrown instead of returning a tuple with {:error, reason}.
With function:
case Repo.transaction(fn -> Repo.insert_all(SomeSchema, inserts) end) do
{:ok, value} -> :ok
{:error, reason} -> :error
end
With Ecto.Multi:
Multi.new()
|> Multi.insert_all("insert", SomeSchema, inserts)
|> Repo.transaction()
|> case do
{:ok, _} -> :ok
{:error, _, _, _} -> :error
end
I know that insert_all/3 has an option (on_conflict: :nothing) to prevent an exception from being raised. Even knowing this, it is a surprise that the code works in an “unsafe” way, why does it work like this?
Investigating the code of the db_connection lib, I noticed that there is a transaction function called by postgrex, and there is a version that runs the code inside a try/catch: db_connection/lib/db_connection.ex at v2.4.1 · elixir-ecto/db_connection · GitHub, so I was assuming that the function was safe.
I appreciate anyone who can help me.
Most Liked
jeremyjh
I think its a valid question. There is a convention in the Elixir standard library that functions which raise exceptions end with a bang, (such as Map.fetch!) and often have a corollary that returns an error tuple.
The difference here, is that the exception is not coming from Repo.transaction. It’s coming from the code you are running inside the thunk; that is your code and your exception. Catching that and turning it into a tuple would be much more surprising to me than raising it.
Maybe the question is really about Repo.insert_all. This function will raise exceptions for all kinds of underlying database conditions including constraint violations, read-only databases, missing tables etc. The conflict behaviour is well-documented in the function docs and is consistent with other Ecto operations. The other exceptions are … well…exceptional. Those are almost always programmer errors and there is no sensible way to handle them other than to crash in my opinion.
josevalim
Non-bang functions never means it will never raise. For example, pass an integer to File.read/1. They will return tagged tuples for some class of errors that are up to the library to decide.
Plus transaction/2 is running your code via a function, so we shouldn’t compare it with File.read/1. A more apt comparison here would be File.open/2, which also doesn’t handle your own exceptions inside the function either.
al2o3cr
That code re-raises everything but the specific :throw on lines 866-868.
Repo.transaction rolls back when an exception occurs but does not stop them. From the docs:
If an unhandled error occurs the transaction will be rolled back and the error will bubble up from the transaction function. If no error occurred the transaction will be committed when the function returns. A transaction can be explicitly rolled back by calling
rollback/1, this will immediately leave the function and return the value given to rollback as{:error, value}.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










