How to return a meaningful error from GenServer's init callback?

I have a GenServer module that might fail to initialize and if it does there is no point in restarting it - it will always fail. So I would like GenServer:start_link(...) to return {:error, :some_reason} in this case.

From gen_server docs

start_link(Module, Args, Options) -> Result
...
If Module:init/1 fails with Reason, the function returns {error,Reason}. If Module:init/1 returns {stop,Reason} or ignore, the process is terminated and the function returns {error,Reason} or ignore, respectively.
Module:init(Args) -> Result
...
If the initialization fails, the function is to return {stop,Reason}, where Reason is any term, or ignore.

This led me to believe that I can solve my problem like this:

iex(1)> defmodule X do
...(1)>   use GenServer
...(1)>
...(1)>   def init(_) do
...(1)>     {:stop, :boom}
...(1)>   end
...(1)> end

iex(2)> GenServer.start_link(X, [])
{:error, :boom}
** (EXIT from #PID<0.101.0>) shell process exited with reason: :boom

If the reason is :normal then it simply returns {:error, :normal} without crashing.

As far as I can understand start_link links the parent with the child process even before init has completed and when {:stop, :anything} is returned from init the process exits abnormally and crashes the parent process that is not able to process the returned value.

I can set the trap_exit to true but I don’t want to trap_exit only for this. I can probably solve this by spawning a different process, trap_exit and start_link from there and then send a message to the parent process with the result but this is obviously a a hack.

Is there a better solution?

it seems that the best I can do is something like this:

defmodule X do
  use GenServer

  def start_link() do
    parent = self()
    spawn_link(fn() -> 
      :erlang.process_flag(:trap_exit, true)
      send parent, {:start_result, GenServer.start_link(X,[])}
    end)

    receive do
      {:start_result, {:ok, pid}} ->
          Process.link(pid)
          {:ok, pid}
      {:start_result, {:error, reason}} ->
          {:error, reason}
    end
  end  

  def init(_) do
    {:stop, :boom}
  end
end

iex(7)> X.start_link()
{:error, :boom}

Do you really need the additional process. I’d just do this:

defmodule MyGenServer do
  use GenServer

  def start_link(arg) do
    old = Process.flag(:trap_exit, true)
    result = GenServer.start_link(__MODULE__, arg)
    Process.flag(:trap_exit, old)
    result
  end
  
  …
end

Is it really important that start_link returns {:error, reason} with a reason different than :normal? Because if not, you can return :ignore from init, or (as you noted) {:stop, :normal}, and you would have your GenServer behave well even if you choose to supervise it.

1 Like

there is a chance that between Process.flag(:trap_exit, true) and Process.flag(:trap_exit, old) something else linked to the main process will crash and that fact will be ignored

that GenServer in init dispatches to a module implementing some specified behaviour (the module is passed as an argument to start_link) and different modules might return different errors so it would be great to tell the actual reason to the caller.

But it seems that there is no way to do it other then trapping exits or exiting with :normal. According to the documentation

The default behaviour when a process receives an exit signal with an exit reason other than normal, is to terminate and in turn emit exit signals with the same exit reason to its linked processes. An exit signal with reason normal is ignored.

it seems that the exit signal is emitted even when init returns {:stop, normal}, but it is ignored by the parent process. And that probably means that there is no way to return anything other then :normal without terminating the caller.

So I decided to go the simpler route - just log the actual reason in init and stop with :normal. And start_link will look like this:

def start_link(...) do
  case GenServer.start_link(...) do
    {:ok, pid} -> {:ok, pid}
    {:error, :normal} -> {:error, :init_error}
  end
end

Not ideal but looks less hacky than the other option.

Thanks for the suggestions!

1 Like