Is Ecto exit not captured by try/rescue?

Roughly, the code

def some_function():
   try do
      # a query which causes ecto to exit
   rescue
      # Some rescue code here which is not getting executed, and is a problem.
   end
end

The shell logs

08:08:20.675 [error] Task #PID<0.793.0> started from #PID<0.789.0> terminating
** (ArgumentError) cannot load `1001` as type {:parameterized, Ecto.Enum, %{embed_as: :self, mappings: [fax: 1003], on_cast: %{"fax" => :fax}, on_dump: %{fax: 1003}, on_load: %{1003 => :fax}, type: :integer}} for field :record_type in %ContactElectronicDetail{__meta__: #Ecto.Schema.Metadata<:loaded, "contact_electronic_detail">, contact_data: nil, contact_id: nil, contact_rank: nil, eleccontact_id: nil, record_type: nil}
    (ecto 3.9.1) lib/ecto/repo/queryable.ex:419: Ecto.Repo.Queryable.struct_load!/6
    (ecto 3.9.1) lib/ecto/repo/queryable.ex:243: anonymous fn/5 in Ecto.Repo.Queryable.preprocessor/3
    (elixir 1.12.3) lib/enum.ex:1582: Enum."-map/2-lists^map/1-0-"/2
    (ecto 3.9.1) lib/ecto/repo/queryable.ex:234: Ecto.Repo.Queryable.execute/4
    (ecto 3.9.1) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
    (ecto 3.9.1) lib/ecto/repo/preloader.ex:272: Ecto.Repo.Preloader.fetch_query/8
    (elixir 1.12.3) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (elixir 1.12.3) lib/task/supervised.ex:35: Task.Supervised.reply/5
    (stdlib 3.14.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Function: &:erlang.apply/2
    Args: [#Function<8.8785771/1 in Ecto.Repo.Preloader.maybe_pmap/3>, [#Function<20.8785771/1 in Ecto.Repo.Preloader.prepare_queries/6>]]

Thanks

Do you have a more concrete example? From your stack trace it looks like the error is happening in a new process that is spawned as a Task. So there is nothing to rescue inside of the process that spawns the task.

The stacktrace in your post looks like it’s from inside the parallel-loading code in Ecto.Repo.preload. That uses Task.async_stream to load multiple tables in parallel, but if any of the tasks crash they terminate the calling process.

One option would be to turn off that behavior by passing in_parallel: false to Repo.preload.

Trapping exits could help, but that’s still going to lead to unexpected {:exit, reason} tuples that Repo.preload may crash on.

1 Like

Ecto is supposed to be started as a supervised task. But my question is in general. Does try/rescue not captures :exit, like the docs says?

The Repo is started as a supervised process (not a task) but queries are executed in the calling process. Anyway, it looks like @al2o3cr got your issue right. IMO trapping exits or rescuing doesn’t help here. The preload query should be as simple as possible so if it’s exiting there is a problem that needs to be fixed. In this case it looks like the field type you are using is not matching the data.

Thanks for the reply.

That uses Task.async_stream to load multiple tables in parallel, but if any of the tasks crash they terminate the calling process.

Just to confirm, by “calling process” do you mean the process which made this Ecto query? And because the calling process itself has crashed, the try/rescue would also not work.

I am going to try is_parallel: false, but what would be the tradeoffs here? I would assume that a table with 6 children associations would need 7 queries that are non-concurrent ?

You are over-focusing on implementation details. Fix the query that’s surfacing this stack trace. You don’t at all need to know how Ecto works in the OTP framework in 95% of the cases.

1 Like

Come on now, nothing wrong with being extra careful. The fix for that bug is dead simple btw.

Come on now, nothing wrong with being extra careful.

In this case I think there might be something wrong :P. You are sacrificing performance to rescue bugs in your code that should rightfully crash.

1 Like

Extra careful against what? Data schema bugs? These should not exist in the first place, your code and database must be always 100% aligned, otherwise your app shouldn’t be live. :person_shrugging:

I think you have misunderstood, I am not going to remove in_parallel. I am asking if there is a way to make try/rescue work here regardless.

The only thing that can exit Ecto mid-transaction is a data schema bug then, is what you are alluding to?
I was not sure what else can go wrong, which is why I have put the try/rescue. If the design space of bugs that can cause Ecto to exit is just bad data schema, then I agree with what you wrote.

There’s a blog post about this here that may be helpful Error handling in Elixir: rescue vs catch | by Tyler Pachal | Medium.

Basically, an exit isn’t an exception, and raise is for handling exceptions. You can catch an exit, but usually you don’t want to. Exits help enforce timeouts and other invariants that are often tied to specific processes, and killing the associated processes is usually important. For example:

Repo.transaction(fn ->
  try do
    # something that invalidates the transaction
  catch
    # if you catch here the whole transaction is still dead anyway at the postgres level, so there's nothing realy to do.
  end
end)
2 Likes

Ah thanks! That looks handy.