Using `dbg/2` to replace `IO.inspect/2` and to pry into code
I’ve been using Elixir’s dbg/2
a little more for debugging.
There are two things I really like about it:
- It’s an improvement on
IO.inspect/2
, and - We can use it to pry into code with
iex
Let’s look at each in turn.
Replacing IO.inspect/2
The first thing it can do is replace (or complement) our IO.inspect/2
usage
when debugging.
Take the following code in a LiveView as an example:
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Post")
|> assign(:post, Blogs.get_post!(id))
end
If we set an IO.inspect/2
at the end of the pipeline, we’ll see the final
socket
value printed out — the one with the “Edit post” page_title
and
the post
assigned.
But what happens if we set a dbg/2
at the end of the
pipeline instead?
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Post")
|> assign(:post, Blogs.get_post!(id))
|> dbg()
end
When we exercise that code, dbg/2
not only prints the last value of the
socket
, but it also prints out the location of the debugging statement
(apply_action/3
) and the socket
at each step of the pipeline:
socket
in its original state,socket
once we set the:page_title
to “Edit Post”,socket
once the:post
is assigned.
Here’s an example:
[lib/scout_web/live/post_live/index.ex:21: ScoutWeb.PostLive.Index.apply_action/3]
socket #=> #Phoenix.LiveView.Socket<
# ...
assigns: %{
# ...
page_title: "Listing Posts",
post: nil,
# ...
},
...
>
|> assign(:page_title, "Edit Post") #=> #Phoenix.LiveView.Socket<
# ...
assigns: %{
__changed__: %{live_action: true, page_title: true},
# ...
page_title: "Edit Post", # <= page title changed
post: nil,
# ...
},
...
>
|> assign(:post, Blogs.get_post!(id)) #=> #Phoenix.LiveView.Socket<
# ...
assigns: %{
__changed__: %{live_action: true, page_title: true, post: true},
# ...
page_title: "Edit Post",
post: %Scout.Blogs.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
id: 228,
body: "some body",
title: "some title",
inserted_at: ~N[2023-01-27 10:32:39],
updated_at: ~N[2023-01-27 10:32:39]
}, # <= post was added
# ...
},
...
As you can see, dbg/2
prints the socket
as we modify it in each pipeline
step. That’s really helpful when debugging!
Prying with iex
We can also use dbg/2
for prying into code when we run it in the context of
iex
.
From there, we can access variables and step through the code. Let’s take a look at another example:
def handle_params(params, _url, socket) do
new_params =
params
|> Map.put("user_id", "23")
|> Map.put("company_id", "46")
|> Map.put("organization_id", "99")
|> dbg()
{:noreply, apply_action(socket, socket.assigns.live_action, new_params)}
end
When we run that code with iex
— say with iex -S mix
phx.server
— we’ll be asked if we want to pry into the code at that point.
Request to pry #PID<0.614.0> at ScoutWeb.PostLive.Index.handle_params/3 (lib/scout_web/live/post_live/index.ex:15)
12: @impl true
13: def handle_params(params, _url, socket) do
14: new_params =
15: params
16: |> Map.put("user_id", "23")
17: |> Map.put("company_id", "46")
18: |> Map.put("organization_id", "99")
Allow? [Yn]
Then, we can inspect variables like params
and socket
.
Allow? [Yn] Y
Interactive Elixir (1.14.0) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> params
%{"id" => "2"}
pry(2)> socket
#Phoenix.LiveView.Socket<
# socket data ...
assigns: %{
# assigns data ...
},
...
>
But that’s not all. We can step through the pipeline with next
(or n
):
13: def handle_params(params, _url, socket) do
14: new_params =
15: params # <= breakpoint -- line is highlighted
16: |> Map.put("user_id", "23")
17: |> Map.put("company_id", "46")
18: |> Map.put("organization_id", "99")
Allow? [Yn] Y
pry(1)> next
iex(1)> params #=> %{"id" => "2"}
# ^ dbg/2 prints the `params` value
Break reached: ScoutWeb.PostLive.Index.handle_params/3 (lib/scout_web/live/post_live/index.ex:16)
13: def handle_params(params, _url, socket) do
14: new_params =
15: params
16: |> Map.put("user_id", "23") # <= breakpoint -- line is highlighted
17: |> Map.put("company_id", "46")
18: |> Map.put("organization_id", "99")
19: |> dbg()
Notice how dbg/2
printed the value of params
after we hit next
. That lets
us see how the params
change at each step in the pipeline.
Let’s hit next
again:
pry(1)> next
iex(1)> |> Map.put("user_id", "23") #=> %{"id" => "2", "user_id" => "23"}
# ^ dbg/2 prints the modified `params` value
Break reached: ScoutWeb.PostLive.Index.handle_params/3 (lib/scout_web/live/post_live/index.ex:17)
14: new_params =
15: params
16: |> Map.put("user_id", "23")
17: |> Map.put("company_id", "46") # <= breakpoint -- line is highlighted
18: |> Map.put("organization_id", "99")
19: |> dbg()
20:
Once we’re done with our prying, we can type continue
to get out of the pry
session and let the rest of the code execute.
Nice, right?
More info
If you want to learn more, take a look at the dbg/2 docs and the debugging with dbg notes in the elixir-lang website.