We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Debugging GenServer state with `:sys.get_state`
almirsarajcic
        
Ever wondered what’s actually inside your GenServer’s state when things go wrong? Instead of adding debug prints or crashing the process, you can peek inside any running GenServer with Elixir’s built-in :sys.get_state/1.
# Peek inside any GenServer without stopping it
:sys.get_state(MyApp.UserCache)
# => %{users: %{"123" => %User{name: "Alice"}}, last_updated: ~U[2024...]}
# Works with PIDs too
pid = GenServer.whereis(MyApp.GameServer)
:sys.get_state(pid)
# => %{players: ["alice", "bob"], status: :waiting, round: 1}
This is incredibly useful for debugging production issues, understanding why your GenServer isn’t behaving as expected, or just exploring how your application’s state evolves over time.
Beyond basic inspection
You can also replace the state entirely for testing scenarios:
# Temporarily modify state for debugging
new_state = %{users: %{}, cache_hits: 0}
:sys.replace_state(MyApp.UserCache, fn _old_state -> new_state end)
# Or just update part of it
:sys.replace_state(MyApp.GameServer, fn state ->
  %{state | status: :game_over}
end)
The :sys module is part of OTP and works with any process that implements the gen_* behaviors. This gives you direct access to process internals without stopping or modifying the running code - essential for debugging live systems.
Testing LiveView socket assigns
This becomes incredibly powerful in tests where you need to inspect LiveView socket assigns:
test "user data loads correctly on mount", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/profile/123")
  # Peek inside the LiveView process to see socket assigns
  view_state = :sys.get_state(view.pid)
  assigns = view_state.socket.assigns
  assert assigns.current_user.id == "123"
  refute assigns.loading
end
test "form assigns update after validation", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users/new")
  # Submit invalid form
  view
  |> form("#user-form", user: %{email: "invalid"})
  |> render_submit()
  # Check the assigns contain validation errors
  view_state = :sys.get_state(view.pid)
  changeset = view_state.socket.assigns.changeset
  refute changeset.valid?
  assert changeset.errors[:email]
end
copied to clipboard
