Catch not matching handle_call

Hi all
I have a GenServer:

defmodule WpOdata.Server.Sap do

  use GenServer

  alias WpOdata.Protocols.Query

  def start_link(_) do
    GenServer.start_link(__MODULE__, nil, [])
  end

  def init(_) do
    {:ok, nil}
  end

  def handle_call({:get, data}, _from, state) do
    IO.puts "Hello"
    {:reply, "Hello", state}
  end

  def handle_info(_msg, state) do
    {:noreply, state}
  end

end

When I send a request does not match to the handle_call like:

GenServer.call(sap, {:post, "hello"})

Then the genserver will be terminate:

iex(1)> [error] GenServer #PID<0.257.0> terminating
** (FunctionClauseError) no function clause matching in WpOdata.Server.Sap.handle_call/3
    (wp_odata) lib/wp_odata/server/sap.ex:15: WpOdata.Server.Sap.handle_call({:post, "hello"}, {#PID<0.184.0>, #Reference<0.0.1.1340>}, nil)
    (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:647: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:post, "hello"}
State: nil

My question is, how to handle this kind of error?

Is there a way to catch all incoming request, that not match handle_call clause?

Thanks

1 Like

As you would do it everywhere else… Use a “catch all” clause… eg. def handle_call(_msg, _from, state), do: # handle it

Especially in a handle_call you should be aware of the fact that returning :noreply-tuple will halt the calling process until you send an answer/reply to (_)from!

Also, usually you should design your public API in a way that there is no need for your caller to ever touch GenServer.call, in fact, your calling site should be unaware of the detail if you implemented on top of a GenServer, Agent or implemented the receive-loop on your own. You should always provide wrapper functions:

defmodule WpOdata.Server.Sap do

  use GenServer

  alias WpOdata.Protocols.Query

  # Public API

  @doc "Starts the SAP-subsystem stuff"
  def start_link(_), do: GenServer.start_link(__MODULE__, nil, [])

  @doc "Retrieves data from SAP"
  def get(query), do: GenSerner.call(__MODULE__, {:get, query})

  # GenServer callbacks

  @doc false
  def init(_) do
    {:ok, nil}
  end

  @doc false
  def handle_call({:get, data}, _from, state) do
    IO.puts "Hello"
    {:reply, "Hello", state}
  end

  @doc false
  def handle_info(_msg, state) do
    {:noreply, state}
  end
end

PS: Maybe you got a mail before with unfinished post, this was because my password storage rampaged and injected my email, then hitting tab, injecting password into nowhere and hitting enter again…

3 Likes

Yes, with a function that takes any message:

def handle_call(_msg, _from, state) do
    {:reply, :unknown_call, state}
end

The real question is, though, do you WANT to? :slight_smile: It is usually preferred/good to fail when an unexpected message is sent. That way you don’t end up in undefined states due to a typo or a missed fixup during a refactor (for a couple common examples).

By failing, your program avoids entering an unhandled/unknown state. Ask yourself: if something randomly calls in with some arbitrary message, what is a valid response? If there is no good, answer, then fail.

This is the “let it crash” philosophy. It isn’t always the Right™ thing, but it usually is. This is the beauty of separated processes: one job / action / task / response handler can die because it was going to enter a bad/unknown/unexpected state, and the rest of the program continues on.

4 Likes

The genserver has no any state, so it is stateless.

So in your opinion, I should let it failed.

I changed the handle_call function to:

  def handle_call({:get, data}, _from, state) do
    {:reply, Query.get(data), state}
  end

The Query is a protocol, so when I would pass a datatype, that does not implement the protocol, then the GenServer will crash like:

iex(10)> Sap.get(sap, "Hello")
[error] GenServer #PID<0.345.0> terminating
** (Protocol.UndefinedError) protocol WpOdata.Protocols.Query not implemented for "Hello"
    (wp_odata) lib/wp_odata/protocols/query.ex:1: WpOdata.Protocols.Query.impl_for!/1
    (wp_odata) lib/wp_odata/protocols/query.ex:8: WpOdata.Protocols.Query.get/1
    (wp_odata) lib/wp_odata/server/sap.ex:20: WpOdata.Server.Sap.handle_call/3
    (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:647: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:get, "Hello"}
State: nil
** (EXIT from #PID<0.263.0>) an exception was raised:
    ** (Protocol.UndefinedError) protocol WpOdata.Protocols.Query not implemented for "Hello"
        (wp_odata) lib/wp_odata/protocols/query.ex:1: WpOdata.Protocols.Query.impl_for!/1
        (wp_odata) lib/wp_odata/protocols/query.ex:8: WpOdata.Protocols.Query.get/1
        (wp_odata) lib/wp_odata/server/sap.ex:20: WpOdata.Server.Sap.handle_call/3
        (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
        (stdlib) gen_server.erl:647: :gen_server.handle_msg/5
        (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

What should I do in this situation?
In addition I would like to send a notification to the sender a message, that something went wrong.

Thanks

1 Like
def handle_call({:get, data}, _from, state} do
  try do
    {:reply, Query.get(data), state}
  rescue
    Protocol.UndefinedError -> {:reply, :badarg, state}
  end
end

Something like this should be sufficient.

2 Likes

Nice thanks

1 Like

Oh, before I forgett!

If Query.get/1 is a (or at least potentially) longrunning function, it might be better to use a :noreply tuple and apawn a worker to not block your GenServer, roughly like this:

def handle_call({:get, data}, from, state} do
  spawn(fn ->
    try do
      send from, Query.get(data)
    rescue
      Protocol.UndefinedError ->
        send from, badarg
    end
  end
  {:noreply, state}
end
1 Like