Gleam Result Chaining
Result chaining connects a sequence of fallible operations so that the first failure stops execution and propagates to the end. You write the happy path as a clean sequence of steps, and the chain automatically handles failures without deeply nested case expressions.
The Nesting Problem Without Chaining
Without chaining — pyramid of doom:
──────────────────────────────────────────────────
case read_file("data.csv") {
Error(e) -> Error(e)
Ok(content) ->
case parse_csv(content) {
Error(e) -> Error(e)
Ok(rows) ->
case validate_rows(rows) {
Error(e) -> Error(e)
Ok(valid) ->
case save_to_db(valid) {
Error(e) -> Error(e)
Ok(saved) -> Ok(saved)
}
}
}
}
With chaining — flat and readable:
──────────────────────────────────────────────────
read_file("data.csv")
|> result.then(parse_csv)
|> result.then(validate_rows)
|> result.then(save_to_db)
result.then — The Core Chain Function
result.then takes a Result and a function. If the result is Ok, it passes the value to the function and returns the function's result. If the result is Error, it skips the function and passes the error forward unchanged.
result.then Diagram
──────────────────────────────────────────────────
Ok(value) |> result.then(f) → f(value) → Ok(new) or Error(e)
Error(e) |> result.then(f) → Error(e) → skips f entirely
import gleam/result
pub fn parse_positive(s: String) -> Result(Int, String) {
int.parse(s)
|> result.map_error(fn(_) { "Not a number: " <> s })
|> result.then(fn(n) {
case n > 0 {
True -> Ok(n)
False -> Error("Must be positive, got: " <> int.to_string(n))
}
})
}
// parse_positive("42") → Ok(42)
// parse_positive("-5") → Error("Must be positive, got: -5")
// parse_positive("abc") → Error("Not a number: abc")
result.map — Transform Without Failure
When a transformation cannot fail, use result.map instead of result.then:
let result =
int.parse("10") // Ok(10)
|> result.map(fn(n) { n * 2 }) // Ok(20) — cannot fail
|> result.map(int.to_string) // Ok("20") — cannot fail
|> result.map_error(fn(_) { "Failed to parse" })
// result = Ok("20")
map vs then Decision
──────────────────────────────────────────────────
Transformation can fail → use result.then
fn returns Result(a, e)
Transformation cannot fail → use result.map
fn returns plain value
result.map_error — Transform the Error
type AppError { ParseFailed(String); DbFailed(String) }
let result =
int.parse(user_input)
|> result.map_error(fn(_) { ParseFailed("Invalid number") })
|> result.then(fn(n) {
db.insert(n)
|> result.map_error(fn(e) { DbFailed(e) })
})
result.try — Gleam's Sugar for then
Some Gleam codebases use use expressions to flatten chained results even further:
pub fn process(id: String) -> Result(String, Error) {
use n <- result.then(int.parse(id))
use user <- result.then(find_user(n))
use data <- result.then(fetch_data(user))
Ok(format(data))
}
The use expression with result.then gives each step a name and eliminates the nested callback structure. The body of the function reads like sequential steps.
Building a Full Chain
import gleam/result
import gleam/io
type OrderError {
InvalidId
OrderNotFound
PaymentFailed(String)
EmailFailed
}
pub fn complete_order(raw_id: String) -> Result(Nil, OrderError) {
int.parse(raw_id)
|> result.map_error(fn(_) { InvalidId })
|> result.then(fn(id) {
find_order(id)
|> result.map_error(fn(_) { OrderNotFound })
})
|> result.then(fn(order) {
charge_card(order)
|> result.map_error(fn(e) { PaymentFailed(e) })
})
|> result.then(fn(order) {
send_receipt(order)
|> result.map_error(fn(_) { EmailFailed })
})
}
pub fn main() {
case complete_order("1042") {
Ok(Nil) -> io.println("Order complete")
Error(InvalidId) -> io.println("Bad order ID")
Error(OrderNotFound) -> io.println("Order not found")
Error(PaymentFailed(msg))-> io.println("Payment failed: " <> msg)
Error(EmailFailed) -> io.println("Could not send receipt")
}
}
Chain Flow for complete_order("1042")
──────────────────────────────────────────────────
"1042" → int.parse → Ok(1042)
→ find_order → Ok(order)
→ charge_card → Error(PaymentFailed("Declined"))
→ send_receipt → SKIPPED (short-circuit)
Final: Error(PaymentFailed("Declined"))
Collecting Results from a List
import gleam/list
import gleam/result
let raw_ids = ["1", "2", "bad", "4"]
// Parse all — fail if any fail:
let all_parsed = list.map(raw_ids, int.parse) |> result.all
// Error(Nil) — "bad" failed
// Parse — keep only successes:
let successes = list.filter_map(raw_ids, int.parse)
// [1, 2, 4]
Key Points
Result Chaining Summary
──────────────────────────────────────────────────
result.then(f) → f gets Ok value; Error passes through
result.map(f) → f cannot fail; wraps result in Ok
result.map_error(f) → transform the Error value
result.unwrap(default) → extract Ok or use fallback
result.all(list) → Ok if all Ok; Error on first failure
use x <- result.then → flatten with use expression
Result chaining transforms error-prone, deeply nested code into a readable, flat sequence of steps. Each step focuses on the happy path; failures propagate automatically. This pattern makes complex multi-step operations as easy to read as a simple list of instructions.
