← Code Compare

Error Handling

On the BEAM, recoverable failures are values you return, not exceptions you throw. The shared task - parse a string to an integer, then compute 100 / n, where parsing can fail and n can be zero - shows two failure points threaded through a result type. Watch the convention split: Erlang, Elixir, and LFE return tagged tuples {ok, _} / {error, _} and pattern-match on them; Gleam has a real Result(a, b) type whose Ok/Error branches the compiler forces you to handle; and Lua, lacking all of this, leans on pcall and multiple return values to signal what went wrong.

Show: ErlangElixirGleamLFELuerl
Erlang
-module(safe_div).
-export([compute/1]).

%% Parse a string, then divide 100 by it. Either step can fail,
%% so each returns the {ok, _} / {error, _} convention.
parse(Str) ->
    case string:to_integer(Str) of
        {N, ""} when is_integer(N) -> {ok, N};
        _ -> {error, not_a_number}
    end.

divide(0) -> {error, divide_by_zero};
divide(N) -> {ok, 100 div N}.

%% Chain the two steps: match the first result before attempting the second.
compute(Str) ->
    case parse(Str) of
        {ok, N} -> divide(N);
        {error, _} = Err -> Err
    end.

%% compute("4")   => {ok, 25}
%% compute("0")   => {error, divide_by_zero}
%% compute("abc") => {error, not_a_number}

Erlang returns {ok, Value} or {error, Reason} from each step and uses case to pattern-match the first result before running the second, so failures short-circuit by being matched and returned unchanged.

Elixir
defmodule SafeDiv do
  # Each step returns the {:ok, _} / {:error, _} convention.
  defp parse(str) do
    case Integer.parse(str) do
      {n, ""} -> {:ok, n}
      _ -> {:error, :not_a_number}
    end
  end

  defp divide(0), do: {:error, :divide_by_zero}
  defp divide(n), do: {:ok, div(100, n)}

  # `with` threads the happy path: each <- must match {:ok, _},
  # otherwise the non-matching value falls straight through to the caller.
  def compute(str) do
    with {:ok, n} <- parse(str),
         {:ok, result} <- divide(n) do
      {:ok, result}
    end
  end
end

SafeDiv.compute("4")   # => {:ok, 25}
SafeDiv.compute("0")   # => {:error, :divide_by_zero}
SafeDiv.compute("abc") # => {:error, :not_a_number}

Elixir keeps the tagged-tuple convention but uses with to flatten the chain: every <- binds an {:ok, _} match, and the first step that doesn't match returns its {:error, _} value directly.

Gleam
import gleam/io
import gleam/int
import gleam/result

pub type DivError {
  NotANumber
  DivideByZero
}

// int.parse already returns Result(Int, Nil); we map its error
// into our own typed error variant.
fn parse(str: String) -> Result(Int, DivError) {
  case int.parse(str) {
    Ok(n) -> Ok(n)
    Error(_) -> Error(NotANumber)
  }
}

fn divide(n: Int) -> Result(Int, DivError) {
  case n {
    0 -> Error(DivideByZero)
    _ -> Ok(100 / n)
  }
}

// `use` (or result.try) chains Results: the function only continues
// while every step is Ok, and the type forces every Error to be handled.
pub fn compute(str: String) -> Result(Int, DivError) {
  use n <- result.try(parse(str))
  divide(n)
}

pub fn main() {
  case compute("4") {
    Ok(value) -> io.println("ok: " <> int.to_string(value))
    Error(NotANumber) -> io.println("not a number")
    Error(DivideByZero) -> io.println("divide by zero")
  }
}

Gleam models failure with the built-in Result(a, b) and a custom DivError type; result.try (here via use) short-circuits on the first Error, and the compiler forces every Ok/Error branch to be handled - there are no exceptions to forget.

LFE
(defmodule safe-div
  (export (compute 1)))

;; Parse a string into (tuple 'ok n) or (tuple 'error reason).
(defun parse (str)
  (case (string:to_integer str)
    ((tuple n "") (when (is_integer n)) (tuple 'ok n))
    (_ (tuple 'error 'not-a-number))))

;; Divide 100 by n, guarding against zero.
(defun divide
  ((0) (tuple 'error 'divide-by-zero))
  ((n) (tuple 'ok (div 100 n))))

;; Chain the steps: match the first result before trying the second.
(defun compute (str)
  (case (parse str)
    ((tuple 'ok n) (divide n))
    ((= (tuple 'error _) err) err)))

;; (compute "4")   => #(ok 25)
;; (compute "0")   => #(error divide-by-zero)
;; (compute "abc") => #(error not-a-number)

LFE keeps Erlang's {ok, _} / {error, _} convention written as (tuple 'ok n) / (tuple 'error reason), matching each step with case and binding the whole error tuple via the (= pattern var) alias to pass it through unchanged.

Luerl
-- Lua has no tagged tuples or Result type, so failures are signalled
-- with multiple return values: (value) on success, (nil, message) on error.
local function parse(str)
  local n = tonumber(str)
  if n == nil or n ~= math.floor(n) then
    return nil, "not a number"
  end
  return n
end

-- Division by zero would yield inf, so check it explicitly and raise.
local function divide(n)
  if n == 0 then
    error("divide by zero")
  end
  return math.floor(100 / n)
end

local function compute(str)
  local n, err = parse(str)
  if n == nil then
    return nil, err
  end
  -- pcall traps the error() from divide and returns ok, result-or-message.
  local ok, result = pcall(divide, n)
  if not ok then
    return nil, result
  end
  return result
end

print(compute("4"))   -- 25
print(compute("0"))   -- nil  divide by zero
print(compute("abc")) -- nil  not a number

Lua signals expected failures with the nil, message multiple-return convention, while pcall traps the exception raised by error() and turns it back into a false, message pair - combining both styles to cover the two failure points.