Handling Errors with Gleam's Algebraic Data Types and Pattern Matching
A practical look at using Gleam's custom types and pattern matching to replace exceptions with compile‑time guarantees.
23 May 2026, 20:00 UTC

The problem: implicit failures in dynamic code
When a function can fail in several ways—network timeout, malformed JSON, missing config—dynamic languages often rely on exceptions or nil checks scattered through the call stack. The compiler cannot tell you whether every failure path is handled, and a missed case becomes a runtime crash.
Thesis: make failure a first‑class value
Gleam solves this by representing every possible outcome as a concrete type. An Algebraic Data Type (ADT) lets you enumerate each variant, and the compiler forces you to handle all of them before the program compiles.
Defining a Result type
The standard library already ships Result(a, e) with two constructors: Ok(a) and Error(e). You can also create domain‑specific ADTs when the error carries more context.
pub type FetchError {
NetworkError
Timeout
InvalidResponse(String)
}
pub type User {
User(id: Int, name: String)
}
pub fn fetch_user(id: Int) -> Result(User, FetchError) {
// … implementation …
Error(NetworkError) // placeholder
}
Each variant is a distinct constructor, so the type checker knows exactly which shapes a FetchError can take.
Exhaustive pattern matching
When you pattern‑match on a Result or any custom ADT, Gleam requires a clause for every constructor. Missing a case produces a compile‑time error.
pub fn handle_user(result: Result(User, FetchError)) -> String {
case result {
Ok(user) -> "User: " <> user.name
Error(NetworkError) -> "Network unavailable"
Error(Timeout) -> "Request timed out"
Error(InvalidResponse(msg)) -> "Bad response: " <> msg
}
}
If you later add a new variant to FetchError (e.g., RateLimited), the compiler will flag handle_user until you add a matching branch.
Chaining with the use keyword
Gleam’s use expression lets you sequence multiple Result-returning functions without nesting case blocks. The compiler rewrites it into the equivalent pattern matches.
pub fn build_profile(user_id: Int) -> Result(Profile, FetchError) {
use user <- fetch_user(user_id)
use posts <- fetch_posts(user.id)
use settings <- fetch_settings(user.id)
Ok(Profile(user, posts, settings))
}
If any step returns Error, the whole function short‑circuits and returns that error, preserving type safety.
Trade‑offs and interop limits
- Verbosity: Compared with Elixir’s
try/rescueor uncheckednil, you write more constructors and match arms. - Erlang interop: Calling untyped Erlang libraries forces you to wrap values in
Dynamicor usegleam/dynamic, which sidesteps compile‑time guarantees. - Learning curve: Developers new to ADTs may initially over‑model simple errors; start with the built‑in
Resultbefore creating custom types.
Actionable next step
Add a small ADT to a current Gleam module, return it from a function, and pattern‑match on the result. Run the compiler with gleam build (no special permissions, just a terminal in the project root) and verify that adding a new variant without updating the match produces an error. This exercise proves the exhaustiveness guarantee in your own codebase.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.