Debugging LiveView applications requires understanding both the server-side processes and client-side updates. This guide covers the tools and techniques available for inspecting and troubleshooting LiveView behavior.
The Debug Module
Phoenix LiveView provides a Phoenix.LiveView.Debug module for runtime introspection of LiveView processes.
Listing LiveViews
Find all currently connected LiveView processes:
iex> Phoenix.LiveView.Debug.list_liveviews()
[
%{
pid: #PID<0.123.0>,
view: MyAppWeb.PostLive.Index,
topic: "lv:phx-12345678",
transport_pid: #PID<0.122.0>
},
%{
pid: #PID<0.124.0>,
view: MyAppWeb.UserLive.Show,
topic: "lv:phx-87654321",
transport_pid: #PID<0.122.0>
}
]
From lib/phoenix_live_view/debug.ex:51-56:
def list_liveviews do
for pid <- Process.list(), dict = lv_process_dict(pid), not is_nil(dict) do
{Phoenix.LiveView, view, topic} = keyfind(dict, :"$process_label")
%{pid: pid, view: view, topic: topic, transport_pid: keyfind(dict, :"$phx_transport_pid")}
end
end
The transport_pid groups LiveViews on the same page, useful for debugging multi-LiveView layouts.
Checking Process Type
Verify if a process is a LiveView:
iex> pid = #PID<0.123.0>
iex> Phoenix.LiveView.Debug.liveview_process?(pid)
true
Inspecting Socket State
Get the socket of a running LiveView:
iex> {:ok, socket} = Phoenix.LiveView.Debug.socket(pid)
iex> socket.assigns
%{
__changed__: %{},
count: 5,
user: %User{id: 1, name: "Chris"},
live_action: :show
}
Accessing the socket returns a snapshot. The LiveView process continues running independently.
Inspecting LiveComponents
Get information about rendered LiveComponents:
iex> {:ok, components} = Phoenix.LiveView.Debug.live_components(pid)
iex> components
[
%{
id: "user-form",
module: MyAppWeb.UserLive.FormComponent,
cid: 1,
assigns: %{
id: "user-form",
user: %User{},
form: %Phoenix.HTML.Form{},
myself: %Phoenix.LiveComponent.CID{cid: 1}
}
}
]
IEx Debugging
Using IEx.pry
Set breakpoints in your LiveView:
def handle_event("save", %{"user" => user_params}, socket) do
require IEx; IEx.pry() # Breakpoint
case Users.update_user(socket.assigns.user, user_params) do
{:ok, user} ->
{:noreply, assign(socket, :user, user)}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
Start your app with:
dbg/2 Pipeline Debugging
Use Elixir 1.14+ dbg/2 for pipeline debugging:
def handle_event("filter", %{"query" => query}, socket) do
users =
socket.assigns.all_users
|> Enum.filter(&String.contains?(&1.name, query))
|> dbg() # Inspect intermediate result
|> Enum.take(10)
{:noreply, assign(socket, :users, users)}
end
Logging
Custom Logging
Add debug logging to track LiveView lifecycle:
defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
require Logger
def mount(_params, _session, socket) do
Logger.debug("Mounting PostLive.Index")
{:ok, socket}
end
def handle_params(params, uri, socket) do
Logger.debug("handle_params: #{inspect(params)}")
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
def handle_event(event, params, socket) do
Logger.debug("Event: #{event}, Params: #{inspect(params)}")
# ... handle event
end
end
Telemetry Events
LiveView emits telemetry events you can hook into:
# In application.ex
def start(_type, _args) do
:telemetry.attach_many(
"liveview-debug",
[
[:phoenix, :live_view, :mount, :start],
[:phoenix, :live_view, :mount, :stop],
[:phoenix, :live_view, :handle_params, :start],
[:phoenix, :live_view, :handle_params, :stop],
[:phoenix, :live_view, :handle_event, :start],
[:phoenix, :live_view, :handle_event, :stop],
[:phoenix, :live_component, :update, :start],
[:phoenix, :live_component, :update, :stop]
],
&MyApp.Telemetry.handle_event/4,
nil
)
# ...
end
defmodule MyApp.Telemetry do
require Logger
def handle_event([:phoenix, :live_view, :mount, :start], _measurements, metadata, _config) do
Logger.info("Mounting #{inspect(metadata.socket.view)}")
end
def handle_event([:phoenix, :live_view, :handle_event, :stop], measurements, metadata, _config) do
Logger.info(
"Event '#{metadata.event}' took #{measurements.duration / 1_000}µs"
)
end
def handle_event(_event, _measurements, _metadata, _config), do: :ok
end
LiveSocket Debug Mode
Enable client-side debugging in assets/js/app.js:
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
debug: true // Enable debug mode
})
This logs all LiveView messages:
live_socket Joining 'lv:phx-12345678'
live_socket Received: {"diff": {"0": {"0": "5"}}}
live_socket Event: {"type": "click", "event": "increment"}
Inspecting Patches
View diffs sent from the server:
// In browser console
window.liveSocket.enableDebug()
// Manually send event
window.liveSocket.execJS(document.querySelector("#my-element"), "[[\"push\",{\"event\":\"click\"}]]")
Latency Simulator
Test with artificial latency:
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
debug: true,
latencySim: 1000 // 1 second delay
})
Common Issues
Memory Leaks
Find LiveViews that aren’t being garbage collected:
iex> liveviews = Phoenix.LiveView.Debug.list_liveviews()
iex> Enum.group_by(liveviews, & &1.view)
%{
MyAppWeb.PostLive.Index => [...], # 1 instance
MyAppWeb.DashboardLive => [...] # 50 instances?! 🚨
}
Infinite Render Loops
Detect when assigns cause re-renders:
def handle_event("update", _, socket) do
Logger.debug("Before: #{inspect(socket.assigns.__changed__)}")
socket = assign(socket, :timestamp, DateTime.utc_now())
Logger.debug("After: #{inspect(socket.assigns.__changed__)}")
{:noreply, socket}
end
Never call assign/3 in render/1 or you’ll create an infinite loop:# BAD: Infinite loop!
def render(assigns) do
assigns = assign(assigns, :time, DateTime.utc_now())
~H"""
{@time}
"""
end
Missing Updates
If updates aren’t appearing:
- Check
phx-update attribute: Ensure containers have correct update strategy
- Verify DOM IDs: Stream items need proper IDs
- Inspect
__changed__: Confirm assigns are marked as changed
iex> {:ok, socket} = Phoenix.LiveView.Debug.socket(pid)
iex> socket.assigns.__changed__
%{} # Nothing changed!
JS Errors
Check browser console for:
Unable to join - Connection issues
unhandled event - Missing event handlers
error: invalid_target - DOM element not found
Testing
LiveView Testing
Use Phoenix.LiveViewTest for integration tests:
test "increments counter", %{conn: conn} do
{:ok, view, html} = live(conn, "/counter")
assert html =~ "Count: 0"
# Simulate click
assert view
|> element("button", "Increment")
|> render_click() =~ "Count: 1"
# Inspect state
assert view |> assign(:count) == 1
end
Async Testing
Debug async operations:
test "loads data asynchronously", %{conn: conn} do
{:ok, view, _html} = live(conn, "/posts")
# Wait for async result
assert view
|> has_element?("#loading")
# Async completes
assert view
|> await_async_result(:posts)
refute view
|> has_element?("#loading")
end
ExProf
Profile LiveView callbacks:
defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
import ExProf.Macro
def handle_event("load", _, socket) do
profile do
posts = Posts.list_posts() # Slow query?
{:noreply, assign(socket, :posts, posts)}
end
end
end
Benchee
Benchmark rendering:
alias Phoenix.LiveView.Renderer
Benchee.run(%{
"render with 10 posts" => fn ->
socket = assign(socket, :posts, Enum.take(all_posts, 10))
Renderer.to_rendered(socket, MyAppWeb.PostLive.Index)
end,
"render with 100 posts" => fn ->
socket = assign(socket, :posts, Enum.take(all_posts, 100))
Renderer.to_rendered(socket, MyAppWeb.PostLive.Index)
end
})
Tips and Tricks
Quick debugging in production:# Attach to running app
iex --sname debug --remsh myapp@hostname
# List LiveViews
iex> Phoenix.LiveView.Debug.list_liveviews()
# Inspect specific LiveView
iex> [lv | _] = Phoenix.LiveView.Debug.list_liveviews()
iex> {:ok, socket} = Phoenix.LiveView.Debug.socket(lv.pid)
iex> socket.assigns
Enable verbose logging:# config/dev.exs
config :logger, level: :debug
config :phoenix, :logger, :debug
Track component updates:def update(assigns, socket) do
IO.inspect(assigns, label: "Component assigns")
{:ok, assign(socket, assigns)}
end
Best Practices
- Use Debug module in development: List and inspect LiveViews regularly
- Add logging to callbacks: Track lifecycle events
- Enable browser debug mode: See client-server communication
- Write tests first: Catch issues before manual debugging
- Profile performance: Measure, don’t guess
- Check telemetry: Hook into built-in instrumentation
Summary
Effective LiveView debugging combines:
- Server inspection:
Phoenix.LiveView.Debug module
- IEx tools:
IEx.pry, dbg/2
- Logging: Custom logs and telemetry
- Browser DevTools: Debug mode and network inspection
- Testing: Automated tests catch regressions
- Profiling: Measure performance bottlenecks
With these tools, you can quickly diagnose and fix issues in your LiveView applications.