DEV Community

Cover image for Debugging with IEx: Interactive Techniques for Elixir Development
João Paulo Abreu
João Paulo Abreu

Posted on

Debugging with IEx: Interactive Techniques for Elixir Development

As we continue exploring IEx capabilities, let's focus on its powerful debugging features. This article will show you practical techniques for debugging Elixir applications using IEx.

Table of Contents

Interactive Debugging with IEx.pry

The IEx.pry/0 function is one of the most powerful debugging tools in Elixir. Let's see it in action with a practical example:

# user_points.ex
defmodule UserPoints do
  require IEx  # Required to use IEx.pry

  def calculate_bonus(points) do
    bonus =
      points
      |> multiply_points()
      |> apply_threshold()
      |> (fn points ->
        IEx.pry()  # Add breakpoint here
        round_points(points)
      end).()

    {:ok, bonus}
  end

  defp multiply_points(points) do
    points * 1.5
  end

  defp apply_threshold(points) when points > 1000 do
    1000
  end

  defp apply_threshold(points), do: points

  defp round_points(points) do
    round(points)
  end
end
Enter fullscreen mode Exit fullscreen mode

To use this in practice:

  1. Save the code in user_points.ex
  2. Start IEx with the code:
iex user_points.ex
Enter fullscreen mode Exit fullscreen mode
  1. Try the function:
iex(1)> UserPoints.calculate_bonus(800)
Break reached: UserPoints.calculate_bonus/1 (user_points.ex:10)
    7:       |> multiply_points()
    8:       |> apply_threshold()
    9:       |> (fn points ->
   10:         IEx.pry() # Add breakpoint here
   11:         round_points(points)
   12:       end).()
   13:

# Let's analyze the flow to this point:
iex(2)> points  # Current value after multiply_points and apply_threshold
1000

# We can also check intermediate calculations:
iex(3)> 800 * 1.5  # What multiply_points did
1200.0

# Since 1200.0 > 1000, apply_threshold returned 1000
iex(4)> continue  # Let's continue execution
{:ok, 1000}  # Final result
Enter fullscreen mode Exit fullscreen mode

At the breakpoint, we can:

  • Inspect variables (like points)
  • Execute code in the current context
  • Use continue to resume execution

Debugging Real Applications

Let's look at a more complex example - a shopping cart system with multiple discount rules:

# shopping_cart.ex
defmodule ShoppingCart do
  defstruct items: [], discounts: [], total: 0

  def new, do: %ShoppingCart{}

  def add_item(cart, item) do
    require IEx; IEx.pry()  # Debug point 1
    %{cart | items: [item | cart.items]}
    |> calculate_total()
    |> apply_discounts()
  end

  def calculate_total(%{items: items} = cart) do
    total = Enum.reduce(items, 0, fn
      %{price: price, quantity: quantity}, acc ->
        acc + (price * quantity)
    end)
    %{cart | total: total}
  end

  def apply_discounts(%{total: total, items: items} = cart) do
    require IEx; IEx.pry()  # Debug point 2
    discounts = [
      quantity_discount(items),
      total_discount(total)
    ]
    %{cart | 
      discounts: discounts,
      total: apply_discount_values(total, discounts)
    }
  end

  defp quantity_discount(items) do
    total_quantity = Enum.reduce(items, 0, & &1.quantity + &2)
    case total_quantity do
      q when q >= 5 -> {:quantity, 0.1}
      _ -> {:quantity, 0}
    end
  end

  defp total_discount(total) do
    case total do
      t when t >= 100 -> {:total, 0.15}
      _ -> {:total, 0}
    end
  end

  defp apply_discount_values(total, discounts) do
    Enum.reduce(discounts, total, fn
      {_type, value}, acc -> acc * (1 - value)
    end)
  end
end
Enter fullscreen mode Exit fullscreen mode

Let's debug this cart system:

# Start IEx with the module
iex(1)> c("shopping_cart.ex")
    warning: redefining module ShoppingCart (current version defined in memory)
[ShoppingCart]

iex(2)> cart = ShoppingCart.new()
%ShoppingCart{items: [], discounts: [], total: 0}

iex(3)> item = %{name: "Phone", price: 500, quantity: 2}
%{name: "Phone", price: 500, quantity: 2}

iex(4)> ShoppingCart.add_item(cart, item)
Break reached: ShoppingCart.add_item/2 (shopping_cart.ex:7)
    4:   def new, do: %ShoppingCart{}
    5:
    6:   def add_item(cart, item) do
    7:     require IEx; IEx.pry()  # Debug point 1
    8:     %{cart | items: [item | cart.items]}
    9:     |> calculate_total()
   10:     |> apply_discounts()

# At Debug point 1:
iex(5)> cart
%ShoppingCart{items: [], discounts: [], total: 0}

iex(6)> item
%{name: "Phone", price: 500, quantity: 2}

iex(7)> continue
Break reached: ShoppingCart.apply_discounts/1 (shopping_cart.ex:22)
   19:   end
   20:
   21:   def apply_discounts(%{total: total, items: items} = cart) do
   22:     require IEx; IEx.pry()  # Debug point 2
   23:     discounts = [
   24:       quantity_discount(items),
   25:       total_discount(total)

# At Debug point 2:
iex(8)> cart.total
1000

iex(9)> continue
%ShoppingCart{
  items: [%{name: "Phone", price: 500, quantity: 2}],
  discounts: [quantity: 0, total: 0.15],
  total: 850.0
}
Enter fullscreen mode Exit fullscreen mode

Advanced Debugging Techniques

1. Using IO.inspect/2

IO.inspect/2 is a non-blocking way to debug your code by inspecting values as they flow through your functions:

def process_order(items) do
  items
  |> IO.inspect(label: "Input items")
  |> calculate_total()
  |> IO.inspect(label: "After total calculation")
  |> apply_discounts()
  |> IO.inspect(label: "After discounts")
end

# Example output:
# Input items: [%{name: "Phone", price: 500, quantity: 2}]
# After total calculation: 1000
# After discounts: 850.0
Enter fullscreen mode Exit fullscreen mode

2. Conditional Debugging

You can use environment variables to control when debugging code runs:

defmodule UserPoints do
  require IEx

  def calculate_bonus(points) do
    bonus =
      points
      |> multiply_points()
      |> debug_threshold()
      |> round_points()

    {:ok, bonus}
  end

  defp debug_threshold(points) do
    if System.get_env("DEBUG") do
      IEx.pry()
    end
    apply_threshold(points)
  end

  # ... rest of the module
end
Enter fullscreen mode Exit fullscreen mode

To use conditional debugging:

# Normal execution
$ iex user_points.ex
iex(1)> UserPoints.calculate_bonus(800)
{:ok, 1000}

# With debugging enabled
$ DEBUG=true iex user_points.ex
iex(1)> UserPoints.calculate_bonus(800)
# Will break at the debug point
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Strategic Breakpoints

    • Place IEx.pry() at critical decision points
    • Remove debugging code before committing
    • Use meaningful variable names at debug points
  2. Effective Debugging Session

    • Start with high-level inspection
    • Check intermediate values
    • Use IO.inspect for passive debugging
    • Keep track of the execution flow
  3. Code Organization

    • Keep debugging code clearly marked
    • Use version control branches for debugging sessions
    • Document discovered issues
    • Clean up debugging code after use
  4. Development Workflow

    • Use IEx.pry() for interactive debugging
    • Use IO.inspect/2 for passive debugging in pipelines
    • Add temporary debugging code in development
    • Use comprehensive tests to prevent bugs

Conclusion

IEx.pry() is a powerful tool for interactive debugging in Elixir. Combined with other techniques like IO.inspect/2 and strategic breakpoints, it provides a robust debugging experience.

Remember that these are development tools - they should be used carefully in production environments.

Next Steps

  • Practice debugging with your own projects
  • Learn about debugging strategies for different types of problems

Top comments (0)