Debugging with IEx: Interactive Techniques for Elixir Development
João Paulo Abreu
Posted on November 20, 2024
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
- Debugging Real Applications
- Advanced Debugging Techniques
- Best Practices
- Conclusion
- Next Steps
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
To use this in practice:
- Save the code in
user_points.ex
- Start IEx with the code:
iex user_points.ex
- 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
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
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
}
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
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
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
Best Practices
-
Strategic Breakpoints
- Place
IEx.pry()
at critical decision points - Remove debugging code before committing
- Use meaningful variable names at debug points
- Place
-
Effective Debugging Session
- Start with high-level inspection
- Check intermediate values
- Use IO.inspect for passive debugging
- Keep track of the execution flow
-
Code Organization
- Keep debugging code clearly marked
- Use version control branches for debugging sessions
- Document discovered issues
- Clean up debugging code after use
-
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
- Use
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
Posted on November 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.