Catching Errors During Supervisor Child Initialization

I have a couple of GenServers that I would like to organize in a supervisor. Some of the GenServers require initialization which can fail (opening ports, reading files, etc.).
What is the best way to include the initialization logic?

My goals:

  • non-blocking initialization
  • automatic execution of init logic, even when Supervisor restarts the GenServer
  • clean and simple error messages that do not require handle_sasl_reports in the config
  • possibility to catch the init error immediately, eg. {:error, reason} = Supervisor.start_link(...)

Do you have any advice for me?

You could start with having your initialization in handle_continue I believe. Though if you want to bring down the supervisor if one of the GenServers crashes then it’s best you leave the initialization code run as early as possible.

Yes, handle_continue ticks the first two goals, but has the same problem as directly using the init callback.
The errors are not shown in logs.
Is there a way to get the init errors reported without handle_sasl_reports? The option makes the logs extremely noisy.

I’d naively assume if you make sure logging is initialized before your stuff you should have normal access to it but I admit I haven’t tried.

handle_continue errors should be logged:

Interactive Elixir (1.15.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Foo do
...(1)>   use GenServer
...(1)>
...(1)>   def start_link(opts \\ []) do
...(1)>     GenServer.start_link(__MODULE__, :ok, opts)
...(1)>   end
...(1)>
...(1)>   def init(:ok) do
...(1)>     {:ok, :ok, {:continue, :what}}
...(1)>   end
...(1)> end
{:module, Foo,
 <<70, 79, 82, 49, 0, 0, 20, 52, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 221,
   0, 0, 0, 50, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, {:init, 1}}
iex(2)> Foo.start_link
{:ok, #PID<0.114.0>}
iex(3)>
09:21:30.653 [error] GenServer #PID<0.114.0> terminating
** (UndefinedFunctionError) function Foo.handle_continue/2 is undefined or private
    Foo.handle_continue(:what, :ok)
    (stdlib 4.0) gen_server.erl:1120: :gen_server.try_dispatch/4
    (stdlib 4.0) gen_server.erl:862: :gen_server.loop/7
    (stdlib 4.0) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:continue, :what}
State: :ok
** (EXIT from #PID<0.108.0>) shell process exited with reason: an exception was raised:
    ** (UndefinedFunctionError) function Foo.handle_continue/2 is undefined or private
        Foo.handle_continue(:what, :ok)
        (stdlib 4.0) gen_server.erl:1120: :gen_server.try_dispatch/4
        (stdlib 4.0) gen_server.erl:862: :gen_server.loop/7
        (stdlib 4.0) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

There are two reports. One is logger and the other is from the linked process crash.

1 Like

Your first and last point contradict each other. You cannot at the same time return successful startup and do init logic async, but also return an error when said async logic is unsuccessful.

Indeed.

To catch an error immediately, you must be waiting to see if there is an error, which means you are blocking.

@LostKobrakai and @benwilson512 Sorry, let me explain:
The root call to start the supervisor has to be synchronous, as you correctly said. What I tried to ask for is that the child processes are initialized concurrently.

@josevalim I see, then I need to check my code again and see why my errors do not appear.