{:stop, "myerror"} not working in GenServer.init/1

The docs for GenServer.init/1 callback say:

Returning {:stop, reason} will cause start_link/3 to return {:error, reason} and the process to exit with reason reason without entering the loop or calling terminate/2.

I can’t get this to work. Here’s my code:

defmodule Foo do
  use GenServer

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

end

{:error, "myerror"} = GenServer.start_link(Foo, nil)

Instead of GenServer.start_link/3 returning {:error, “myerror”} it simply crashes instead:

** (EXIT from #PID<0.70.0>) "myerror"

What am I doing wrong? Thanks for the help.

start_link will link the calling process to the GenServer. When it fails to init returning {:stop, reason} tuple the GenServer will exit with the provided reason. This will cause all the linked processes to exit as well.

If the caller process is trapping exits, start_link will return the error tuple and the EXIT message will be delivered to the inbox.

1 Like

I see.

What’s the general strategy for when your GenServer represents a network connection to some server? I want MyModule.start_link to return {:error, reason} if the TCP connection fails or the handshake protocol fails.

Should the connection/handshaking all be done in MyModule.start_link before calling GenServer.start_link?

Thanks again.

The init callback of a GenServer that represents a connection should normally just set up some initial state, then return :ok to let application startup proceed as usual. To handle the business of actually connecting, a timeout is returned or a delayed message is sent, that will then be handled by the GenServer when it’s started up, at which point it should try to connect. If it fails, it would normally be prudent to retry after a period of time, preferably with a backoff mechanism.

Connection and DBConnection are examples of this behavior, and would be a good starting point.

See this great article for some of the reasoning behind it:

http://ferd.ca/it-s-about-the-guarantees.html

2 Likes

It adds so much complexity…

{:ok, conn} = MyClient.start_link
MyClient.some_api(conn)

What should MyClient.some_api return if it’s not connected? {:error, :notconnected}? Should it block until it is connected?

If the former, then how would the above code ever work? You’d have to introduce a new call such as…

{:ok, conn} = MyClient.start_link
MyClient.wait_till_connected(conn)
MyClient.some_api(conn)

Which makes the API clunky imo. I guess you could build into each api call the wait_till_connected call or something, but again, adding a bunch of complexity. ¯\_(ツ)_/¯

Also, there is a lot of state associated with the connection I’m managing (it’s a pub/sub client). If the client is responsible for reconnecting, it’s going to have to resubscribe to all the subjects that it was subscribed to before the disconnect (I’m guessing that’s a reasonable and intuitive expectation?).

I’m not trying to be lazy. I guess I don’t know the expectations of how this client should behave and so I’m trying to stick to the simplest solution until that’s not good enough anymore.

Well, I don’t know what you intend to use the client for… If you just need to keep state, you don’t need to keep a GenServer; just return a struct, for example, with the state (including any connection reference and whatnot):

defmodule MyClient do
  defstruct conn: nil, ...

  # ...
end

{:ok, client} = MyClient.new
MyClient.some_api(client)

…which then could, of course, return an {:error, :not_connected}, OR block until connected, maybe depending on a keyword argument you give.