Gleam's `use` Expression: Flat Pipelines Without Exceptions
Gleam has no exceptions, so fallible code can turn into a staircase of case expressions. The `use` expression flattens it — here's how the sugar works, with a result.try pipeline and its trade-offs.
22 Apr 2026, 22:32 UTC

Gleam has no exceptions and no null. Every fallible operation returns a Result(a, e) — either Ok(value) or Error(reason) — and the compiler forces you to handle both. That's great for reliability, but a chain of five fallible steps written with nested case expressions turns into a staircase of indentation that hides the actual logic.
Gleam's answer is the use expression: a small piece of syntactic sugar that lets sequential, fallible code read top-to-bottom, the way it would in a language with ? or do-notation — without adding any new control-flow machinery to the language.
The problem: the case-expression staircase
Suppose you're loading a config file: read the file, parse it as JSON, decode the fields. Each step can fail. With plain case, the happy path drifts right:
case read_file("config.json") {
Ok(text) ->
case json.parse(text) {
Ok(json) ->
case decode_config(json) {
Ok(config) -> Ok(start_server(config))
Error(e) -> Error(e)
}
Error(e) -> Error(e)
}
Error(e) -> Error(e)
}Three steps and you're already four levels deep. The error branches are pure boilerplate — every one of them just passes the error upward.
What `use` actually is
use is not a keyword with special semantics. It desugars to a plain function call where the rest of the enclosing block becomes the final callback argument:
use x <- f(a, b)
// desugars to:
f(a, b, fn(x) { ...everything after the use line... })That's the whole feature. Any function whose last parameter is a callback can be called with use. The standard library's gleam/result module provides result.try, which unwraps an Ok and passes the value to your callback — or, on Error, returns the error immediately without ever calling it. That gives you early return built out of an ordinary function.
The worked example
The same config loader with use and result.try:
import gleam/result
pub fn load_config() -> Result(Config, LoadError) {
use text <- result.try(read_file("config.json"))
use json <- result.try(parse_json(text))
use config <- result.try(decode_config(json))
Ok(config)
}Now the happy path is a straight vertical line, and any step that returns Error short-circuits the whole block with the original error value intact — no re-wrapping, no lost context.
The same pattern works for resource scoping. A library author can write a with_connection function that opens a connection, calls your callback with it, and guarantees cleanup afterwards:
use conn <- with_connection("localhost:5432")
query(conn, "SELECT ...")Because the callback is the rest of the block, the resource's lifetime is delimited by the code structure itself — the same bracket pattern you'd see in Haskell or OCaml, expressed with one line of sugar.
Trade-offs and limitations
The flexibility cuts both ways. Since use works with any function taking a trailing callback, the reader can't tell from the call site what happens to that callback. result.try calls it zero or one times. But list.map also has a trailing callback — use x <- list.map(items) runs the rest of the block once per element, which looks deceptively like a single binding. Overusing use with non-obvious functions hides control flow, so teams often adopt a convention: reserve it for early-return helpers and resource-scoping wrappers.
Two more caveats. First, use is not async/await — on the BEAM target, concurrency is actor-style processes and message passing, a completely different model. Second, this is a young language: use was added after the earliest releases, and stdlib module layouts have shifted across 0.x and 1.x. Treat any snippet (including this one) as tied to a toolchain version.
Verify it yourself
Install the current toolchain and check gleam --version, then in a fresh gleam new demo project paste the pipeline above with stub functions. The decisive test: make the middle step return Error(Nil) and assert in a gleeunit test that the final Ok branch never runs and the error propagates unchanged. Confirm gleam/result's current API against the docs shipped with your toolchain rather than trusting any blog's module paths.
The takeaway: use gives Gleam flat, readable error handling without exceptions or new runtime semantics — just a callback and a convention. Reach for it when a block is a linear sequence of fallible steps, and be deliberate about which functions you let sit on the right of the <-.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.