Debugging
Need to debug Elixir code? Use IEx.pry, dbg, Observer, and logging for effective debugging.
Prerequisites
- Basic Elixir syntax
- Understanding of processes and the BEAM
- Completed Beginner Tutorial
Problem
Debugging functional, concurrent applications requires different tools than traditional imperative debugging. You need to inspect pipelines, trace messages between processes, monitor system resources, and understand failure cascades in supervision trees.
Challenges:
- Inspecting intermediate values in pipelines
- Understanding process crashes and restarts
- Tracking messages between processes
- Identifying performance bottlenecks
- Debugging production issues without stopping the system
Solution
Leverage IEx.pry for breakpoints, dbg for pipeline inspection, Observer for system visualization, and Logger for production debugging.
How It Works
1. IEx.pry - Interactive Breakpoints
defmodule MyApp.Calculator do
require IEx
def complex_calculation(data) do
step1 = transform(data)
IEx.pry() # Execution stops here, IEx session opens
step2 = validate(step1)
IEx.pry() # Another breakpoint
finalize(step2)
end
defp transform(data), do: data * 2
defp validate(data), do: max(data, 0)
defp finalize(data), do: data + 1
endIn IEx:
iex> step1
20
iex> step1 * 3
60
iex> respawn()Key commands:
whereami()- Show current location in coderespawn()- Continue executionbreak!- Set breakpoints dynamically
2. dbg - Pipeline Debugging
result = [1, 2, 3]
|> Enum.map(&(&1 * 2))
|> Enum.filter(&(&1 > 2))
|> Enum.sum()
result = [1, 2, 3]
|> Enum.map(&(&1 * 2))
|> dbg() # Shows: [2, 4, 6]
|> Enum.filter(&(&1 > 2))
|> dbg() # Shows: [4, 6]
|> Enum.sum()
|> dbg() # Shows: 10Advanced dbg usage:
def process(user) do
dbg(user.name) # Just the name
dbg(user.age > 18) # Boolean result
user
|> prepare()
|> dbg() # Entire pipeline step
|> save()
end3. IO.inspect - Quick Inspection
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "After map")
|> Enum.sum()
|> IO.inspect(label: "Final result")
%User{name: "Alice", age: 30}
|> IO.inspect(limit: :infinity, pretty: true)4. Logger - Production Debugging
require Logger
defmodule MyApp.UserService do
def create_user(params) do
Logger.debug("Creating user with params: #{inspect(params)}")
case validate(params) do
{:ok, validated} ->
Logger.info("User validation successful", user_id: validated.id)
save_user(validated)
{:error, changeset} ->
Logger.warning("User validation failed",
errors: inspect(changeset.errors),
params: inspect(params)
)
{:error, changeset}
end
rescue
exception ->
Logger.error("User creation crashed",
exception: Exception.format(:error, exception, __STACKTRACE__)
)
reraise exception, __STACKTRACE__
end
endStructured logging:
config :logger,
backends: [:console],
compile_time_purge_matching: [
[level_lower_than: :info] # Remove debug in prod
]
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id, :user_id, :module, :function]
Logger.info("User logged in",
user_id: 123,
ip_address: "192.168.1.1",
user_agent: "Mozilla/5.0"
)5. Observer - System Visualization
:observer.start()Observer features:
- System tab: CPU, memory, process count
- Load Charts: Real-time resource graphs
- Applications: Supervision tree structure
- Processes: All processes, sort by memory/reductions
- Table Viewer: ETS/Mnesia tables
- Trace Overview: Trace function calls
Remote observation:
iex --name prod@host --cookie secret
iex --name debug@local --cookie secret
Node.connect(:"prod@host")
:observer.start()6. Tracing with :sys
{:ok, pid} = MyServer.start_link()
:sys.trace(pid, true)
:sys.trace(pid, false)
:sys.get_state(pid)
:sys.suspend(pid)
:sys.resume(pid)7. Erlang’s :dbg Module
:dbg.tracer()
:dbg.tp(MyModule, :my_function, :cx)
:dbg.p(pid, [:call, :return_to])
:dbg.p(:all, :call)
:dbg.stop_clear()Example - trace user creation:
:dbg.tracer()
:dbg.tp(MyApp.Accounts, :create_user, [])
:dbg.p(:all, :call)
MyApp.Accounts.create_user(%{name: "Bob"})8. Process Information
Process.list()
Process.info(pid)
Process.info(pid, :messages) # Message queue
Process.info(pid, :memory) # Memory usage
Process.info(pid, :current_stacktrace)
Process.whereis(MyApp.Server)
Process.registered()9. Recon - Production Debugging
{:recon, "~> 2.5"}
:recon.proc_count(:memory, 10)
:recon.proc_count(:reductions, 10)
:recon.info(pid)
:recon.port_info()10. ExUnit Debugging
defmodule MyTest do
use ExUnit.Case
test "debugging with IEx" do
result = some_function()
require IEx; IEx.pry() # Inspect during test
assert result == :expected
end
# Run single test with debugging
# mix test path/to/test.exs:10
endVariations
Remote Console for Production
bin/my_app remote
iex --remsh my_app@hostnameCustom Inspect Protocol
defmodule User do
defstruct [:id, :name, :password_hash]
end
defimpl Inspect, for: User do
def inspect(user, _opts) do
"#User<id: #{user.id}, name: #{user.name}, password: [REDACTED]>"
end
end
IO.inspect(%User{id: 1, name: "Alice", password_hash: "secret"})Crash Dump Analysis
:erlang.halt(1)
erl_crash.dump
crashdump_viewer.start()Advanced Patterns
1. Distributed Debugging
Node.connect(:"node_b@host")
pid = :rpc.call(:"node_b@host", Process, :whereis, [MyServer])
:sys.get_state(pid)2. Debugging LiveView
defmodule MyAppWeb.PageLive do
use Phoenix.LiveView
require Logger
def mount(_params, _session, socket) do
Logger.debug("LiveView mounted", socket_id: socket.id)
if connected?(socket), do: Logger.info("WebSocket connected")
{:ok, assign(socket, count: 0)}
end
def handle_event("inc", _params, socket) do
Logger.debug("Increment event", current: socket.assigns.count)
{:noreply, update(socket, :count, &(&1 + 1))}
end
end3. Memory Leak Detection
before = :erlang.memory()
run_operation()
after_mem = :erlang.memory()
IO.inspect(after_mem[:total] - before[:total], label: "Memory delta")
:recon_alloc.memory(:allocated)4. Deadlock Detection
waiting = Process.list()
|> Enum.map(&{&1, Process.info(&1, :current_function)})
|> Enum.filter(fn {_pid, {:current_function, {mod, fun, _}}} ->
mod == :gen_server and fun == :loop
end)
IO.inspect(waiting, label: "Processes waiting")Use Cases
Development:
- Understanding pipeline transformations
- Testing error handling paths
- Learning library behavior
- Debugging test failures
Production:
- Investigating slow requests
- Finding memory leaks
- Analyzing crashes
- Monitoring system health
Performance:
- Identifying bottlenecks
- Profiling function calls
- Measuring resource usage
- Optimizing hot paths
Troubleshooting
IEx.pry Not Working
require IEx
iex -S mix phx.server --trace
IEx.break!(MyModule, :function_name, 2) # arity 2Observer Crashes
brew install wxwidgets
apt-get install libwxgtk3.0-devCan’t See Logs
Logger.configure(level: :debug)
Application.get_env(:logger, :backends)Best Practices
Remove debug code before commit:
# Use mix format to spot forgotten IEx.pry mix format --check-formattedUse appropriate log levels:
debug- Development onlyinfo- Important eventswarning- Degraded stateerror- Failures
Structured logging in production:
Logger.info("User action", user_id: id, action: "purchase") # Better than: Logger.info("User #{id} made purchase")Use Observer on QA, not production: Observer’s GUI can impact performance
Set up remote access securely:
# Use SSH tunnel ssh -L 9001:localhost:9001 production-serverDon’t log sensitive data:
Logger.info("Login attempt", user: sanitize(user))
Common Pitfalls
- Forgetting to remove IEx.pry: Breaks production
- Over-logging: Fills disk, impacts performance
- Not using structured logging: Hard to parse logs
- Ignoring process limits: Observer shows current limits
- Debugging prod without supervision: Always use supervised sessions
Performance Impact
Logger.info("Event occurred") # Async
IO.inspect(large_data) # Blocks
:dbg.p(:all, :call) # Traces everything
:observer.start() # GUI + polling