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.

Leave a Comment

Your email address will not be published. Required fields are marked *