Reading — step 1 of 7
Learn
While gen_server handles general stateful processes, gen_statem specializes in finite state machines. Erlang/OTP's docs treat it as the modern replacement for the older gen_fsm.
When use gen_statem
When your server has explicit STATES with different behavior in each:
- TCP connection (closed → connecting → established → closing)
- Order processing (cart → confirmed → shipped → delivered)
- Authentication flow (anonymous → challenged → authenticated)
- Game session (waiting → playing → paused → finished)
The state name dispatches to different handler functions. Cleaner than a switch over state field in a gen_server.
Module skeleton
-module(door).
-behaviour(gen_statem).
-export([start_link/0, knock/0, open/0, close/0]).
-export([init/1, callback_mode/0, handle_event/4, terminate/3]).
%% Public API
start_link() -> gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).
knock() -> gen_statem:call(?MODULE, knock).
open() -> gen_statem:cast(?MODULE, open).
close() -> gen_statem:cast(?MODULE, close).
%% gen_statem callbacks
init([]) -> {ok, closed, none}.
callback_mode() -> handle_event_function.
%% Closed state
handle_event(cast, open, closed, Data) ->
io:format("opening the door~n"),
{next_state, opened, Data};
handle_event({call, From}, knock, closed, Data) ->
{next_state, closed, Data, [{reply, From, knock_knock}]};
%% Opened state
handle_event(cast, close, opened, Data) ->
io:format("closing the door~n"),
{next_state, closed, Data};
handle_event({call, From}, knock, opened, Data) ->
{next_state, opened, Data, [{reply, From, already_open}]};
handle_event(_Event, _Msg, State, Data) ->
{next_state, State, Data}.
terminate(_Reason, _State, _Data) -> ok.
Two callback modes
state_functions — one function per state:
closed(EventType, EventContent, Data) -> ...
opened(EventType, EventContent, Data) -> ...
handle_event_function (shown above) — single handler dispatched on state:
handle_event(EventType, EventContent, State, Data) -> ...
Pick one in callback_mode/0. State functions read better for many states; handle_event scales for complex dispatch.
Event types
{call, From}— synchronous call fromgen_statem:call/2cast— async fromgen_statem:cast/2info— generic message (from!)internal— events generated within the state machinetimeout— event timer firedstate_timeout— state-specific timeout
Common return values
{next_state, NewState, NewData}
{next_state, NewState, NewData, Actions}
{keep_state, NewData}
{keep_state_and_data, Actions}
{stop, Reason, NewData}
Actions can include replies (for calls), timers, postpone, etc.
When NOT to use gen_statem
- Simple stateful servers with no distinct states — use gen_server
- Pure functional state — no process needed at all
- Very few states — a gen_server with a state field is fine
Comparison
| gen_server | gen_statem | |
|---|---|---|
| State | One blob | Named states with own handlers |
| Dispatch | Manual | Automatic by state |
| Verbosity | Less for simple servers | Less for complex state machines |
| Use case | Workers, caches, registries | Protocol handlers, workflows, games |
Common mistakes
- gen_statem for non-state-machine code — overkill. gen_server is simpler when there's no state-based dispatch.
- Forgetting callback_mode/0 — required, or the module won't start.
- Mixing event types — calls and casts behave differently. Pick the right one for each operation.
- Returning wrong tuple shape — gen_statem expects specific tuples. Refer to docs for exact return values.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…