Gleam Result Type

The Result type represents an operation that can succeed or fail. It carries either a success value or an error value. Instead of exceptions that crash your program or error codes you forget to check, Result makes every failure explicit and forces you to handle it.

The Definition of Result

type Result(success, error) {
  Ok(success)
  Error(error)
}

Result Diagram
──────────────────────────────────────────────────
Result(Int, String) can be:

┌──────────────┐    OR    ┌─────────────────────┐
│   Ok(42)     │          │  Error("not found") │
│ success = 42 │          │  error description  │
└──────────────┘          └─────────────────────┘

Ok holds the success value. Error holds the failure reason. Both carry data — the error value can describe exactly what went wrong.

Functions That Return Result

import gleam/int

// Parsing a string as an integer
let good = int.parse("42")       // Ok(42)
let bad  = int.parse("hello")    // Error(Nil)

// Division — fails if divisor is zero
import gleam/float
let result = float.divide(10.0, 0.0)   // Error(Nil)
let fine   = float.divide(10.0, 2.0)   // Ok(5.0)

Handling Result with case

let input = "123"

let message = case int.parse(input) {
  Ok(n)      -> "Parsed: " <> int.to_string(n * 2)
  Error(Nil) -> "Could not parse the number"
}
// "Parsed: 246"

case on Result
──────────────────────────────────────────────────
int.parse("123") → Ok(123)

case Ok(123) {
  Ok(n)      → n = 123 → "Parsed: 246"
  Error(Nil) → skipped
}

Custom Error Types

The error variant can be any type — a string, a custom type, or an integer code:

type LoginError {
  WrongPassword
  UserNotFound
  AccountLocked(reason: String)
}

pub fn login(username: String, password: String) -> Result(User, LoginError) {
  case find_user(username) {
    None -> Error(UserNotFound)
    Some(user) ->
      case check_password(user, password) {
        False -> Error(WrongPassword)
        True  -> Ok(user)
      }
  }
}

result.map — Transform the Success Value

import gleam/result

let parsed = int.parse("50")         // Ok(50)
let doubled = result.map(parsed, fn(n) { n * 2 })  // Ok(100)

let failed = int.parse("bad")        // Error(Nil)
let still_err = result.map(failed, fn(n) { n * 2 }) // Error(Nil)

result.map Diagram
──────────────────────────────────────────────────
Ok(50)      ──map(×2)──▶  Ok(100)
Error(Nil)  ──map(×2)──▶  Error(Nil)  (unchanged)

result.then — Chaining Fallible Operations

Chain multiple operations that each return Result. If any step fails, the chain short-circuits:

import gleam/result

pub fn parse_and_sqrt(input: String) -> Result(Float, String) {
  int.parse(input)
  |> result.map_error(fn(_) { "Not a number" })
  |> result.then(fn(n) {
       case n >= 0 {
         True  -> Ok(float.square_root(int.to_float(n)))
         False -> Error("Cannot sqrt a negative number")
       }
     })
}

// parse_and_sqrt("16") → Ok(4.0)
// parse_and_sqrt("-4") → Error("Cannot sqrt a negative number")
// parse_and_sqrt("hi") → Error("Not a number")

Chain Flow
──────────────────────────────────────────────────
input = "16"
  │
  ├── int.parse("16")  → Ok(16)
  │
  ├── 16 >= 0? True → Ok(sqrt(16.0)) → Ok(4.0)
  │
  └── Final: Ok(4.0)

input = "hi"
  │
  ├── int.parse("hi") → Error(Nil)
  │
  └── Short-circuit → Error("Not a number")

result.unwrap — Extract with a Default

import gleam/result

let n = result.unwrap(int.parse("99"), 0)    // 99
let d = result.unwrap(int.parse("bad"), 0)   // 0

Collecting Multiple Results

import gleam/list
import gleam/result

let inputs = ["1", "2", "three", "4"]
let parsed = list.map(inputs, int.parse)
// [Ok(1), Ok(2), Error(Nil), Ok(4)]

let all_ok = result.all(parsed)
// Error(Nil)  — because one element failed

Result vs Option


When to Use Each
──────────────────────────────────────────────────
Use Option(a) when:
  → Something either exists or does not
  → Absence is normal and expected
  → No reason needed for the "nothing" case
  Example: searching for a user, getting list head

Use Result(a, e) when:
  → An operation can succeed or fail
  → The error carries meaningful information
  → You want to describe WHY something failed
  Example: parsing input, network request, file I/O

Practical Example — File Parser

import gleam/result
import gleam/int
import gleam/string

type ParseError {
  EmptyInput
  InvalidFormat(String)
  OutOfRange(Int)
}

pub fn parse_age(input: String) -> Result(Int, ParseError) {
  case string.trim(input) {
    "" -> Error(EmptyInput)
    trimmed ->
      case int.parse(trimmed) {
        Error(_) -> Error(InvalidFormat(trimmed))
        Ok(age) ->
          case age >= 0 && age <= 150 {
            True  -> Ok(age)
            False -> Error(OutOfRange(age))
          }
      }
  }
}

pub fn describe_result(r: Result(Int, ParseError)) -> String {
  case r {
    Ok(age)                -> "Valid age: " <> int.to_string(age)
    Error(EmptyInput)      -> "Please enter an age"
    Error(InvalidFormat(s)) -> "'" <> s <> "' is not a number"
    Error(OutOfRange(n))   -> int.to_string(n) <> " is not a valid age"
  }
}

Key Points


Result Essentials
──────────────────────────────────────────────────
1. Result(ok, err) → Ok(value) or Error(reason)
2. The error type can be anything — String, custom type
3. Handle with case — compiler enforces both branches
4. result.map transforms Ok values safely
5. result.then chains fallible operations
6. result.unwrap extracts with a fallback
7. Use Result when failure needs a reason; Option otherwise

The Result type makes error handling a first-class concern. Every function that can fail says so in its signature, every caller handles both outcomes, and no failure slips through unnoticed. This produces software that is genuinely reliable — not just software that happens to work in the happy path.

Leave a Comment

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