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.
