Understanding Expression Blocks in Gleam

When transitioning to Gleam from languages like Elixir, Rust, or JavaScript, it is common to run into syntax hurdles when trying to run side effects (like logging) before returning a fallback value. If you try to sequence multiple expressions inside a case clause without grouping them, Gleam's compiler will throw an error because each case arm expects exactly one expression.

Why the Original Code Fails

In Gleam, a case clause follows the pattern Pattern -> Expression. When you write:

Error(e) ->
  e
  |> error_label
  |> io.println_error
  []

The compiler does not treat these as two sequential statements. Instead, it expects a single expression right after the arrow ->.

Furthermore, attempting to pipe the result of io.println_error into list.new fails because io.println_error returns Nil. The pipe operator (|>) injects that Nil as the first argument, but list.new takes zero arguments, resulting in an arity mismatch.

The Solution: Wrap Multiple Expressions in a Block

To execute a side effect (such as printing an error to standard error) and then return an empty list [], wrap the body of your case arm in curly braces { ... }. A block evaluates all expressions in sequence and evaluates to the value of the final expression:

import gleam/io
import gleam/list
import simplifile

fn list_files(dir: String) -> List(String) {
  case simplifile.read_directory(at: dir) {
    Ok(entries) ->
      entries
      |> list.filter(fn(name: String) {
        case simplifile.is_file(dir <> name) {
          Ok(True) -> True
          _ -> False
        }
      })

    Error(e) -> {
      e
      |> error_label
      |> io.println_error

      // This final expression defines the return value of the block
      []
    }
  }
}

Idiomatic Alternatives in Gleam

While wrapping expressions in { ... } solves the problem directly, Gleam offers several functional patterns that can make this logic even cleaner:

1. Use result.unwrap or result.map

If you separate logging from value extraction, you can leverage the standard gleam/result module:

import gleam/result

fn list_files(dir: String) -> List(String) {
  simplifile.read_directory(at: dir)
  |> result.map(fn(entries) {
    list.filter(entries, fn(name) {
      simplifile.is_file(dir <> name) == Ok(True)
    })
  })
  |> result.lazy_unwrap(fn() {
    io.println_error("Failed to read directory")
    []
  })
}

2. Safer Path Concatenation

As an added tip, using string concatenation (dir <> name) can easily cause path bugs if dir does not end with a trailing slash. Consider using filepath.join(dir, name) from the filepath package for robust cross-platform path handling.

Summary

  • Each case arm in Gleam expects a single expression.
  • To run multiple expressions (such as logging an error and returning a default value), group them inside a block { ... }.
  • The value returned by the block will be the value of its last expression.