Eliminating Runtime Crashes with Gleam's Result Type
Learn how Gleam uses the Result type and the 'use' keyword to replace dangerous exceptions with compile-time safety and linear error handling.
20 Nov 2025, 07:24 UTC

The Cost of Hidden Failures
\nIn many languages, a function signature like find_user(id: Int) -> User is a lie. It claims to return a user, but it might actually throw a NullPointerException or a DatabaseError that crashes the process. These \"hidden\" failure modes force developers to wrap everything in generic try-catch blocks, often catching errors they didn't anticipate or missing ones they should have handled.
Gleam solves this by making failure a first‑class citizen of the type system. Instead of throwing exceptions, Gleam uses the Result type. If a function can fail, its return type must explicitly say so. The takeaway is simple: you cannot access a successful value without first acknowledging the possibility of failure.
Explicit Error Handling via Result
\nA Result type is a generic type that can be one of two variants: Ok(value) or Error(error). This structure shifts the burden of error checking from the runtime to the compiler.
To get the data out of a Result, you use pattern matching. Because the Gleam compiler tracks these types, it will refuse to compile your code if you handle the Ok case but forget the Error case. This eliminates the \"forgot to check for null\" class of bugs entirely.
Managing Sequences with the 'use' Keyword
\nHandling every single Result with a case expression can quickly lead to a \"pyramid of doom\", where your code drifts further right with every nested check. To solve this, Gleam provides the use keyword.
The use keyword acts as syntactic sugar for piping. When used with a function that returns a Result, it allows you to \"unwrap\" the success value for the rest of the block. If the function returns an Error, the rest of the block is skipped, and the error is returned immediately from the parent function. This creates a clean, linear flow similar to async/await or do-notation in other functional languages.
Worked Example: User Validation Pipeline
\nConsider a scenario where we need to validate a user ID, fetch a user from a database, and check if that user is active. Each step can fail for different reasons.
\n// Define custom error types for clarity
pub type UserError {
InvalidId
NotFound
InactiveAccount
}
fn validate_id(id: Int) -> Result(Int, UserError) {
case id <= 0 {
True -> Error(InvalidId)
False -> Ok(id)
}
}
fn fetch_user(id: Int) -> Result(String, UserError) {
// Simulated DB lookup
case id == 42 {
True -> Ok(\"Alice\")
False -> Error(NotFound)
}
}
fn check_active(name: String) -> Result(String, UserError) {
case name == \"Alice\" {
True -> Ok(name)
False -> Error(InactiveAccount)
}
}
pub fn get_active_user(id: Int) -> Result(String, UserError) {
// The 'use' keyword handles short-circuiting automatically
use validated_id <- validate_id(id)
use user_name <- fetch_user(validated_id)
use active_user <- check_active(user_name)
Ok(active_user)
}
\nVerification and Behavior
\nTo verify this behavior, run the get_active_user function with different inputs. If you pass -1, validate_id returns Error(InvalidId), and the subsequent fetch_user and check_active calls are never executed. The final result is Error(InvalidId).
Trade-offs and Limitations
\nThe primary trade-off is verbosity. Developers coming from Python or JavaScript may find it tedious to explicitly define error types and wrap return values in Ok(). There is no \"global\" exception handler to catch everything; every single failure point must be accounted for in the type signature.
Additionally, while use simplifies linear flows, complex branching logic (e.g., \"try this, and if it fails, try that\") still requires manual pattern matching, which can become verbose when dealing with deeply nested custom error types.
Practical Implementation
\nWhen implementing this pattern in your own project, follow these steps:
\n- \n
- Define a custom type for your errors rather than using strings. This allows the caller to pattern match on specific error cases. \n
- Use the
usekeyword for sequential operations to keep the indentation flat. \n - Avoid
unwrap-style patterns (if available via libraries) in production code; always prefer explicit pattern matching to ensure the compiler can guarantee safety. \n
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.