Gleam Error Handling

Error handling in Gleam is explicit and type-safe. Errors are values — they appear in function signatures, flow through the type system, and must be handled by the caller. No exceptions, no surprise crashes from unhandled failures.

The Core Philosophy


Other languages — invisible failures:
──────────────────────────────────────────────────
try {
  let data = fetchData()    // might throw
  process(data)             // might throw
} catch (e) {               // catches everything — or nothing
  handle(e)
}

Gleam — explicit failures:
──────────────────────────────────────────────────
case fetch_data() {
  Ok(data)  -> process(data)    // success path
  Error(e)  -> handle(e)        // failure path — required
}

Returning Errors

A function that can fail returns Result(OkType, ErrorType):

type DatabaseError {
  ConnectionFailed
  QueryTimeout
  RecordNotFound(id: Int)
}

pub fn find_user(id: Int) -> Result(User, DatabaseError) {
  case db_query("SELECT * FROM users WHERE id = " <> int.to_string(id)) {
    Ok([])   -> Error(RecordNotFound(id))
    Ok(rows) -> Ok(row_to_user(rows))
    Error(_) -> Error(ConnectionFailed)
  }
}

Handling Errors with case

case find_user(42) {
  Ok(user)                 -> show_profile(user)
  Error(RecordNotFound(id)) -> show_404(id)
  Error(ConnectionFailed)  -> show_500()
  Error(QueryTimeout)      -> retry_later()
}

The compiler verifies that every variant of DatabaseError is handled. Adding a new error variant to the type causes compile errors in every case that does not cover it — a powerful safety net.

Propagating Errors with result.then

When multiple operations can fail, chain them rather than nesting case expressions:

import gleam/result

// Three operations, each can fail:
pub fn process_order(order_id: Int) -> Result(Receipt, AppError) {
  find_order(order_id)
  |> result.then(fn(order) { validate_order(order) })
  |> result.then(fn(order) { charge_payment(order) })
  |> result.then(fn(order) { send_confirmation(order) })
}

result.then Chain
──────────────────────────────────────────────────
find_order(1)   → Ok(order)
  │
  └─▶ validate_order(order) → Ok(order)
        │
        └─▶ charge_payment(order) → Error(PaymentFailed)
                                       │
                                       └─▶ short-circuits
                                           Final: Error(PaymentFailed)

Converting Error Types

When combining results from different systems with different error types, use result.map_error to normalize them:

type AppError {
  DbError(String)
  NetworkError(String)
  ValidationError(String)
}

pub fn load_user_data(id: Int) -> Result(User, AppError) {
  find_user(id)
  |> result.map_error(fn(e) { DbError(format_db_error(e)) })
}

pub fn fetch_avatar(user: User) -> Result(Bytes, AppError) {
  http_get(user.avatar_url)
  |> result.map_error(fn(e) { NetworkError(e.message) })
}

Providing Fallbacks with result.unwrap

let user_name =
  find_user(id)
  |> result.map(fn(u) { u.name })
  |> result.unwrap("Anonymous")
// If Ok(user) → user.name
// If Error(_) → "Anonymous"

Collecting Multiple Results

import gleam/list
import gleam/result

let inputs = ["42", "bad", "17", "99"]
let parsed = list.map(inputs, int.parse)
// [Ok(42), Error(Nil), Ok(17), Ok(99)]

// Collect all — fails if any failed:
let all = result.all(parsed)
// Error(Nil) — because one failed

// Keep only the successes:
let successes = list.filter_map(inputs, int.parse)
// [42, 17, 99]

Logging Errors Without Failing

import gleam/result

pub fn safe_parse(s: String) -> Int {
  int.parse(s)
  |> result.inspect_error(fn(e) {
       io.println("Parse failed for: " <> s)
     })
  |> result.unwrap(0)
}

Practical Example — File Processing Pipeline

import gleam/result
import gleam/list
import gleam/io

type ProcessError {
  ParseError(line: Int)
  EmptyFile
  InvalidHeader
}

pub fn process_csv(raw: String) -> Result(List(Row), ProcessError) {
  let lines = string.split(raw, "\n")
  case lines {
    []           -> Error(EmptyFile)
    [header, ..data_lines] ->
      case validate_header(header) {
        False -> Error(InvalidHeader)
        True  ->
          data_lines
          |> list.index_map(fn(line, i) { parse_row(line, i + 2) })
          |> result.all
      }
  }
}

pub fn main() {
  let csv = "name,age\nAlice,30\nBob,25"
  case process_csv(csv) {
    Ok(rows)                -> io.debug(rows)
    Error(EmptyFile)        -> io.println("No data")
    Error(InvalidHeader)    -> io.println("Bad format")
    Error(ParseError(line)) -> io.println("Error on line " <> int.to_string(line))
  }
}

Key Points


Error Handling Essentials
──────────────────────────────────────────────────
1. Errors are values — Result(Ok, Error) type
2. All failure paths appear in the function signature
3. case forces handling both Ok and Error branches
4. result.then chains multiple fallible operations
5. result.map_error normalizes error types
6. result.unwrap extracts with a safe fallback
7. result.all collects a list of Results into one
8. No exceptions — no surprise crashes

Gleam's error handling forces clarity. Every function that can fail announces it. Every caller that receives a failure handles it. The compiler enforces this contract throughout your entire codebase, making the error handling story auditable at a glance.

Leave a Comment

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