madili

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/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

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

josevalim

Creator of Elixir

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

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}.

aziz

aziz

That’s actually fine and allowed. Maybe you misremember something… :slight_smile:

The way it works may come as a surprise to you, but it’s not really unsafe, or is it? Any exception that occurs inside a transaction, whether due to the database or Elixir, will cause a rollback of all changes and in that sense it is rather safe. So your idea of safety here seems to be that you expect transaction/2 to always return an error tuple when something went wrong including when an exception was raised.

Why does it work like that? I think it has to do with the concept and the nature of exceptions. The philosophy is namely to let them “bubble” up and not to suppress/convert them at an arbitrary point. Imagine transaction/2 caught all of them and always returned an error tuple. In that case you wouldn’t be able to have an outer/wrapping try/1 statement that listens on certain exceptions. What’s more, such exceptions wouldn’t be reported to the bug database by libraries like Sentry. Unless you pattern-match and re-raise, of course, but you’d have to remember to do that every time you call the transaction function. So in general it’s better to allow exceptions to bubble up.

However, considering how some other functions work you do probably have a point. For example, Jason.decode/2 always returns an error tuple containing the exception struct when there was an error. But there’s the counterpart Jason.decode!/2 which raises in case of errors. Ecto has insert/2 and insert!/2. Should it also have two transaction functions in the same way? Seems reasonable, but it would be a huge breaking change I believe. Maybe you’d like to comment, @josevalim?

If you need to have a “safe” transaction function I’d suggest to add a safe_transaction/2 function to your Repo module which just catches all exceptions and converts them to an error tuple. :+1:

thiagomajesk

thiagomajesk

Now I wish we’d had a transaction!/2 alternative because letting it raise now means that one would have to spread try/catch’es all over the code for those edge cases, isn’t it? I heard that this could be considered kind of an antipattern exactly because of the premises you exposed.

BTW, the docs say this about the bang convention:

A trailing bang (exclamation mark) signifies a function or macro where failure cases raise an exception.

It doesn’t seem to make a distinction from where or how far down the call stack the exception is raised but actually if the failure case raises an exception or not.

Since transaction/2 abstracts the underlying code, why should I care about what happens inside of it? BTW, you also could make the same argument for the File.read/1 function, because the error doesn’t actually come from inside the function but from erlang / disk - the code must be abstracted away from implementation details somewhere, don’t you think? It seems that it depends on how abstract your code should be.

And now I’m also curious how does Ecto.Multi come to play if this is the expected behavior? I always thought that Ecto.Multi was meant to allow you to do “safe” operations (steps) and treat any error that prevents your operation from succeeding - which is still kinda true. But since there are cases that simply raise, this means that one cannot just match on :error and expect that it would just work, right? I’ve successfully used Ecto.Multi in the past and now I’m thinking about unexpected edge cases left behind.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

We're in Beta

About us Mission Statement