GenServer

GenServer (generic server) is the core OTP behaviour for stateful processes.

32.1 A complete GenServer

defmodule Stack do
  use GenServer

  # ---- Client API ----
  def start_link(initial \\ []), do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  def push(item), do: GenServer.cast(__MODULE__, {:push, item})
  def pop, do: GenServer.call(__MODULE__, :pop)
  def peek, do: GenServer.call(__MODULE__, :peek)

  # ---- Server callbacks ----
  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_call(:pop, _from, [h | t]), do: {:reply, {:ok, h}, t}
  def handle_call(:pop, _from, []), do: {:reply, {:error, :empty}, []}
  def handle_call(:peek, _from, state), do: {:reply, List.first(state), state}

  @impl true
  def handle_cast({:push, item}, state), do: {:noreply, [item | state]}

  @impl true
  def handle_info(msg, state) do
    IO.inspect(msg, label: "unexpected message")
    {:noreply, state}
  end

  @impl true
  def terminate(reason, _state), do: IO.puts("Terminating: #{inspect(reason)}")
end

32.2 Callbacks and return values

{:ok, state}, {:ok, state, {:continue, term}}, :ignore, {:stop, reason} GenServer.call (sync)

{:reply, reply, state}, {:noreply, state}, {:stop, reason, reply, state} GenServer.cast (async)

{:noreply, state}, {:stop, reason, state} any other message (timers, monitors, raw send) {:noreply, state} {:continue, term} stop (only if trapping exits or stopped explicitly) {:ok, new_state} status/crash reports

Extras in returns: a timeout ({:reply, r, s, 5000} → :timeout message), :hibernate, {:continue, arg}.

32.3 Deferred replies

def handle_call(:slow, from, state) do
  Task.start(fn -> GenServer.reply(from, compute()) end)
  {:noreply, state}
end

32.4 Periodic work

def init(state) do
  schedule()
  {:ok, state}
end

def handle_info(:work, state) do
  do_work()
  schedule()
  {:noreply, state}
end

defp schedule, do: Process.send_after(self(), :work, :timer.minutes(1))

32.5 Expensive initialization with handle_continue

def init(args), do: {:ok, %{data: nil}, {:continue, :load}}
def handle_continue(:load, state), do: {:noreply, %{state | data: load_from_db()}}

32.6 Naming

GenServer.start_link(M, arg, name: :atom_name)
GenServer.start_link(M, arg, name: {:global, :name})
GenServer.start_link(M, arg, name: {:via, Registry, {MyRegistry, "key"}})

32.7 GenServer pitfalls

A GenServer processes one message at a time — it can become a bottleneck.

Don't use a GenServer just to organize code; use modules/functions. Use processes for *runtime* concerns: state, concurrency, isolation.

call from a GenServer to itself deadlocks.

Keep handle_* fast; offload heavy work to Tasks.

Calls have a default 5 s timeout.