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.
-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.
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.
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.
(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.
-- 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 numberLua 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.