The right way to do this is not to intercept the exits in your channel, but instead have another process that follows you. When you go down, it can trigger a callback. Below is a snippet to get you started:
# lib/my_app.ex children = [ ... worker(ChannelWatcher, [:rooms]) ] # web/channels/room_channel.ex def join("rooms:", <> id, params, socket) do uid = socket.assigns.user_id] :ok = ChannelWatcher.monitor(:rooms, self(), {__MODULE__, :leave, [id, uid]}) {:ok, socket} end def leave(room_id, user_id) do # handle user leaving end # lib/my_app/channel_watcher.ex defmodule ChannelWatcher do use GenServer ## Client API def monitor(server_name, pid, mfa) do GenServer.call(server_name, {:monitor, pid, mfa}) end def demonitor(server_name, pid) do GenServer.call(server_name, {:demonitor, pid}) end ## Server API def start_link(name) do GenServer.start_link(__MODULE__, [], name: name) end def init(_) do Process.flag(:trap_exit, true) {:ok, %{channels: HashDict.new()}} end def handle_call({:monitor, pid, mfa}, _from, state) do Process.link(pid) {:reply, :ok, put_channel(state, pid, mfa)} end def handle_call({:demonitor, pid}, _from, state) do case HashDict.fetch(state.channels, pid) do :error -> {:reply, :ok, state} {:ok, _mfa} -> Process.unlink(pid) {:reply, :ok, drop_channel(state, pid)} end end def handle_info({:EXIT, pid, _reason}, state) do case HashDict.fetch(state.channels, pid) do :error -> {:noreply, state} {:ok, {mod, func, args}} -> Task.start_link(fn -> apply(mod, func, args) end) {:noreply, drop_channel(state, pid)} end end defp drop_channel(state, pid) do %{state | channels: HashDict.delete(state.channels, pid)} end defp put_channel(state, pid, mfa) do %{state | channels: HashDict.put(state.channels, pid, mfa)} end end