Bare receive inside an OTP callback.
An OTP process is already sitting in a receive loop owned by its behaviour
module. A receive written inside a callback runs inside that loop and
selectively consumes from the same mailbox, which breaks three things at
once:
- It steals messages the behaviour needs.
{:system, _, _}is how:sys.get_state/1,:sys.suspend/1,:sys.replace_state/2and the whole debug and trace surface work;{:EXIT, _, _}is how a trapping process learns a link died;{:DOWN, ...}is every monitor in flight. A receive with a catch-all clause eats them. - It reorders delivery. Messages the callback does not match stay in the queue and are re-scanned on every later receive — the selective receive cliff, quadratic in queue length.
- Without an
after, it can block forever. The process stops answering its supervisor, so shutdown waits out the child's timeout and then brutal-kills it, turning a graceful stop into a lost buffer.
There is no compiler or dialyzer diagnostic for this, and it survives
review because the receive usually looks locally reasonable.
Precision
call_edge deliberately treats closure construction as a call so that
reachability follows execution into lambdas passed to Enum.map,
Task.async and friends. That is right in general and wrong here: a
receive inside spawn(fn -> ... end) runs in the spawned process, not
the callback's. Closure edges are subtracted, so what remains is control
that stays on this process's stack.