Try and Rescue: Turning Failures into Clear Results

Welcome. In this course, you will learn how Elixir handles errors and builds fault-tolerant systems. Since this is our first unit, we will start with exceptions and how to convert them into predictable return values using try/rescue. This pattern is useful when you want to keep a function pure and return {:ok, value} or {:error, reason} instead of crashing.

Handling Arithmetic Errors with Try/Rescue
defmodule ErrorHandler do
  def safe_divide(a, b) do
    try do
      result = a / b
      {:ok, result}
    rescue
      ArithmeticError -> {:error, "Cannot divide by zero"}
      e -> {:error, "Unexpected error: #{inspect(e)}"}
    end
  end
end

IO.inspect(ErrorHandler.safe_divide(10, 2))
IO.inspect(ErrorHandler.safe_divide(10, 0))

What it does:

  • The try block attempts a / b.
  • If b is 0, Elixir raises an ArithmeticError, which we rescue and convert to {:error, "Cannot divide by zero"}.
  • The second rescue clause matches any other exception, binds it to e, and returns a friendly error string. This keeps the function’s contract stable: always return {:ok, value} or {:error, reason}.

Why it matters:

  • In production, a simple math mistake should not bring down your process. Converting exceptions to tagged tuples makes failures explicit and easy to handle with pattern matching.

Note: rescue matches exceptions; catch is for low-level Erlang “throws” and is rarely needed in day-to-day Elixir. For example, you might see throw/catch used for non-local returns in legacy code, but idiomatic Elixir almost always prefers exceptions and rescue. Prefer rescuing specific exception types when possible.

Parsing Input Safely and Inspecting Results
defmodule ErrorHandler do
  def parse_integer(string) do
    try do
      {:ok, String.to_integer(string)}
    rescue
      ArgumentError -> {:error, "Invalid integer format"}
    end
  end
end

IO.inspect(ErrorHandler.parse_integer("42"))
IO.inspect(ErrorHandler.parse_integer("abc"))

What it does:

  • String.to_integer/1 raises ArgumentError on bad input (like "abc"). We rescue that and return {:error, "Invalid integer format"}.
  • IO.inspect shows the outcomes:
    • ErrorHandler.safe_divide(10, 2) -> {:ok, 5.0}
    • ErrorHandler.safe_divide(10, 0) -> {:error, "Cannot divide by zero"}
    • ErrorHandler.parse_integer("42") -> {:ok, 42}
    • ErrorHandler.parse_integer("abc") -> {:error, "Invalid integer format"}

Design takeaway:

  • This approach gives you a clean, uniform interface. Callers can pattern match on {:ok, value} or {:error, reason} without worrying about exceptions leaking out.

Note: Exceptions are relatively “heavy.” Use try/rescue when interacting with code that raises (like parsing or I/O). For ordinary control flow, prefer returning {:ok, ...} | {:error, ...} directly. In production OTP systems, it’s often better to “let it crash” and rely on supervisors to restart failed processes, rather than catching every exception. In later units, you will see how supervisors help you “let it crash” safely when that’s the better choice.

Summary and What Comes Next

You learned how to:

  • Use try/rescue to catch exceptions and return stable tuples within a module.
  • Rescue specific exception types (ArithmeticError, ArgumentError) and provide clear error messages.
  • Keep your public API predictable by avoiding raised exceptions at call sites.
  • Prefer “let it crash” with supervision for many real-world errors, using try/rescue mainly for boundary cases or when interacting with code that raises.

You are ready to practice transforming exceptions into friendly results and inspecting them. The next step will be hands-on — let’s put this pattern to work and build your confidence.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal