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.

Want my latest thoughts, posts, and projects in your inbox?

    I will never send you spam. Unsubscribe any time.