The use Expression
- When several operations each return
Result, nestingcaseexpressions quickly becomes unreadable. use x <- f(...)rewrites the rest of the block as a callback passed tof.- Any function that accepts a callback as its last argument works with
use. - Combined with
result.try,useeliminates deeply nested error-handling code.
The Nesting Problem
- When you call several functions that each return
Result, you need to unwrap each one before using it - A single
caseis fine; three in a row is hard to read
let ok_result = case parse_int("30") {
Error(reason) -> Error(reason)
Ok(total) ->
case parse_int("6") {
Error(reason) -> Error(reason)
Ok(divisor) ->
case safe_divide(total, divisor) {
Error(reason) -> Error(reason)
Ok(quotient) -> validate_positive(quotient)
}
}
}
io.println(string.inspect(ok_result))
Ok(5)
Error("division by zero")
parse_intconverts a string to an integer, returningErrorif the string is invalidsafe_dividedivides two integers, returningErrorif the denominator is zerovalidate_positivechecks that a number is positive, returningErrorif it is not- Every step adds another level of indentation
- The actual computation (
validate_positive(quotient)) is buried at the innermost level
- The actual computation (
- When any step fails, every outer
Error(reason) -> Error(reason)arm just re-wraps and propagates the same error without adding any information
let err_result = case parse_int("30") {
Error(reason) -> Error(reason)
Ok(total) ->
case parse_int("0") {
Error(reason) -> Error(reason)
Ok(divisor) ->
case safe_divide(total, divisor) {
Error(reason) -> Error(reason)
Ok(quotient) -> validate_positive(quotient)
}
}
}
io.println(string.inspect(err_result))
- Replacing
"6"with"0"causessafe_divideto return an error- The two outer arms still just pass the error along unchanged
Flattening with use
userewrites nested callback as a flat sequence of steps- This is called syntactic sugar because it sweetens the language
- Translating the easy-to-read form into the basic form is called desugaring
- This is what passes for wit among language designers
result.try(result, callback)callscallbackwith the value insideOk(x), or returnsErrorunchanged without calling the callback at all
let ok_result = {
use total <- result.try(parse_int("30"))
use divisor <- result.try(parse_int("6"))
use quotient <- result.try(safe_divide(total, divisor))
validate_positive(quotient)
}
io.println(string.inspect(ok_result))
Ok(5)
Error("division by zero")
- Each
useline extracts the value fromOkand binds it to a name - If any step returns
Error, the whole block short-circuits and produces that error immediately- The remaining steps are never evaluated
- Because when
result.tryreceives anError, it returns it directly without calling the callback
- The final expression is the result of the whole block
- Compare the two versions: the
useform reads like a plain sequence of steps
let err_result = {
use total <- result.try(parse_int("30"))
use divisor <- result.try(parse_int("0"))
use quotient <- result.try(safe_divide(total, divisor))
validate_positive(quotient)
}
io.println(string.inspect(err_result))
parse_int("0")succeeds (zero is a valid integer), sodivisoris bound to0safe_divide(30, 0)returnsError("division by zero")- The block short-circuits there;
validate_positiveis never called
How use Works
useis not special-cased forResult: it is a general rewrite ruleuse x <- f(a, b)followed by a block of code is equivalent to:
f(a, b, fn(x) {
body
})
- The
xinfn(x)is exactly the same binding as thexintroduced byuse x <-- The rewrite is one-for-one, so the name carries over directly
- In other words: call
fwith its normal arguments plus one extra argument, a function that takesxand contains the rest of the block - The function that receives the callback decides what to do with it
result.trycalls the callback only when its first argument isOk(x), and propagatesErrorunchanged otherwise
- Any function whose last parameter is a callback works with
use - If the callback takes no arguments, omit the binding:
use <- f(a, b)
use Beyond Results
- Because
useis just a rewrite, it works with any function that accepts a callback
with_greeting("long form", fn(greeting) { io.println(greeting) })
{
use greeting <- with_greeting("with use")
io.println(greeting)
}
with_greetingtakes a name and a callback, and passes the constructed greeting to the callback- Both forms produce the same output
usejust removes the anonymous function syntax
- The
useform expands to exactly the same call as the explicit version:
with_greeting("with use", fn(greeting) {
io.println(greeting)
})
{
use item <- list.each([1, 2, 3])
io.println(int.to_string(item))
}
Hello, long form!
Hello, with use!
1
2
3
list.eachcalls the callback once for each element of the listuse item <- list.each([1, 2, 3])is equivalent to writinglist.each([1, 2, 3], fn(item) { ... })- The rest of the block becomes the body of the anonymous function
- No curly braces because it's a single expression
Guidelines for use
- Use
use x <- result.try(...)when chaining three or more fallible operations- For one or two operations, a
caseexpression is often clearer
- For one or two operations, a
- Prefer
result.tryfor chaining; useresult.maponly for single-step transformations usedoes not catch errors; it propagates them exactly as nestedcasewould- Any function that takes a callback as its last argument works with
use, not just functions from theresultmodule - Do not nest
useblocks insideuseblocks; if you feel tempted to do so, extract a helper function instead
Further Reading
- The Gleam language tour covers
usewith additional examples, including a discussion of how it relates to monadic bind in other languages.
Check Understanding
What does use x <- result.try(expr) desugar to?
It desugars to:
result.try(expr, fn(x) {
rest of block
})
result.try calls the callback with the value inside Ok(x),
or returns the Error unchanged without calling the callback at all.
The use line just removes the need to write the anonymous function explicitly.
Can you use use with a function that returns something other than Result?
Yes.
use works with any function whose last parameter is a callback.
The function controls what it does with that callback:
result.try calls it only on success,
list.each calls it once per element,
and a custom function can call it however it likes.
The Result type has no special relationship to use.
If result.try were replaced by a function that always calls its callback, could the block still short-circuit?
No.
Short-circuiting is not a property of use itself — it is a property of result.try.
use only rewrites syntax; whether the callback is called, skipped, or called multiple times depends entirely on what the receiving function does.
A function that always calls its callback will always execute the rest of the block, regardless of how many use lines appear.
Exercises
Identify the callback (5 minutes)
Rewrite this use expression as an explicit callback without use:
let result = {
use name <- result.try(find_user(id))
use score <- result.try(fetch_score(name))
Ok(score * 2)
}
What type must find_user and fetch_score return for this to compile?
Three-step chain (15 minutes)
Write three functions:
read_config(path: String) -> Result(String, String) (returns Error if the path is empty),
parse_port(raw: String) -> Result(Int, String) (wraps int.parse with a descriptive error),
and validate_port(port: Int) -> Result(Int, String) (returns Error if the port is outside 1–65535).
Chain all three with use so that read_config("") short-circuits before the other two are called.
Custom use target (15 minutes)
Write with_default(maybe: Option(a), default: a, callback: fn(a) -> b) -> b
that calls callback with the value inside Some, or with default if the option is None.
Then use it with use to unwrap an Option(Int) and double the result,
falling back to 0 when the option is None.