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.
