Intermediate

I/O Operations in Gleam

Learn console output, formatted printing, file reading and writing, and Result-based I/O error handling in Gleam with Docker-ready examples

Input and output are where a program meets the outside world. In Hello World you called io.println once; here we go further — formatting values for the console, writing to standard error, and reading and writing files.

Gleam’s take on I/O is shaped by its design. There is no printf and no format-string statement. Because the language is statically typed with no variadic formatting, you produce output by building strings explicitly — joining pieces with the <> operator and converting values with module functions like int.to_string and float.to_string. The gleam/io module, part of the standard library, handles the console: println and print for standard output, plus println_error and print_error for standard error.

Anything that touches the filesystem lives outside the core standard library, in small focused packages. File access uses simplifile, which you add to a project with gleam add simplifile. And crucially, Gleam has no exceptions: every operation that can fail — reading a missing file, parsing a number — returns a Result value that you pattern match on with case. That makes error handling part of the type system rather than an afterthought. This tutorial covers formatted console output, file reading and writing, and Result-based error handling, all runnable in Docker.

Formatted Console Output

Since Gleam has no printf, formatted output is just string building. You convert each value to a String and concatenate. Create a file named io_output.gleam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import gleam/io
import gleam/int
import gleam/float
import gleam/string

pub fn main() {
  // println writes a line to stdout; print omits the trailing newline
  io.println("Standard output, one line at a time.")
  io.print("No newline here... ")
  io.println("continued on the same line.")

  // There is no printf — build the string and convert values explicitly
  let name = "Ada"
  let age = 36
  io.println("Name: " <> name <> ", Age: " <> int.to_string(age))

  // Convert numbers with the int and float modules
  io.println("Pi is approximately " <> float.to_string(3.14159))
  io.println("Half of 3 is " <> float.to_string(3.0 /. 2.0))

  // The string module shapes output — repeat, uppercase, and more
  io.println(string.repeat("=", 20))
  io.println(string.uppercase("done"))

  // println_error writes to standard error instead of standard output
  io.println_error("This diagnostic goes to stderr")
}

The <> operator concatenates strings, and each value is turned into text by its module: int.to_string for integers, float.to_string for floats. Note /. — Gleam uses distinct operators for float (/.) and integer (/) division, so the types never mix silently. println_error sends its output to standard error, which keeps diagnostics separate from your program’s real output.

Writing and Reading Files

File I/O comes from the simplifile package rather than the standard library. Every function returns a Result, so reading and writing are the same shape as any other fallible operation. Create a file named io_files.gleam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import gleam/io
import gleam/int
import gleam/list
import gleam/string
import simplifile

pub fn main() {
  let path = "notes.txt"

  // write creates the file, or overwrites it if it already exists
  let _ = simplifile.write(to: path, contents: "First line\nSecond line\n")

  // append adds to the end without overwriting what is already there
  let _ = simplifile.append(to: path, contents: "Appended line\n")

  // read the whole file back as a single String
  case simplifile.read(from: path) {
    Ok(contents) -> {
      io.println("--- " <> path <> " ---")
      io.print(contents)
    }
    Error(_) -> io.println("Could not read " <> path)
  }

  // Build a file from a list of lines, then read and number each one
  let langs = ["Gleam", "Erlang", "Elixir"]
  let _ = simplifile.write(to: "languages.txt", contents: string.join(langs, "\n"))

  case simplifile.read(from: "languages.txt") {
    Ok(contents) -> {
      io.println("--- languages.txt ---")
      contents
      |> string.split("\n")
      |> list.index_map(fn(line, i) { int.to_string(i) <> ": " <> line })
      |> list.each(io.println)
    }
    Error(_) -> io.println("Could not read languages.txt")
  }
}

simplifile.write and simplifile.append use labelled arguments (to: and contents:) so the call reads like a sentence, and both hand you back a Result(Nil, FileError) — here we discard it with let _ =. Reading returns Result(String, FileError), which case forces you to handle. Once the text is in memory, ordinary Gleam pipelines take over: string.split breaks it into lines, list.index_map pairs each line with its index, and list.each prints them.

Handling I/O Errors with Result

Because failures are values, not exceptions, you handle a missing file the same way you handle any other Result — by matching its Error case. Create a file named io_errors.gleam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import gleam/io
import simplifile

pub fn main() {
  // Create a file we know exists
  let _ = simplifile.write(to: "present.txt", contents: "hello")

  // Reading a file that exists succeeds with Ok
  case simplifile.read(from: "present.txt") {
    Ok(contents) -> io.println("Read present.txt: " <> contents)
    Error(_) -> io.println("Could not read present.txt")
  }

  // Reading a missing file returns Error — nothing is thrown, execution continues
  case simplifile.read(from: "missing.txt") {
    Ok(contents) -> io.println("Read missing.txt: " <> contents)
    Error(error) ->
      io.println("Could not read missing.txt: " <> simplifile.describe_error(error))
  }
}

The second read fails, but the program does not crash. The Error branch receives a FileError value describing what went wrong, and simplifile.describe_error turns it into a human-readable message (for a missing file, "No such file or directory."). This is the heart of Gleam’s error story: there is no try/catch, no stack unwinding, and no way to forget a failure case — the compiler will not let a case on a Result compile unless you handle both Ok and Error.

Reading Interactive Input

Reading a line typed at a terminal is deliberately not part of Gleam’s standard library, and it is uncommon in BEAM programs, which more often receive input over the network or from files. When you do need it, a dedicated package such as stdin provides it (gleam add stdin), returning the same Result-shaped values you have seen above. For batch programs, reading from a file — as shown in the previous sections — is the usual way Gleam takes input, and it keeps the examples fully reproducible.

Running with Docker

Like Hello World, each example is copied into a fresh Gleam project and run. The file examples also run gleam add simplifile to pull in the file-I/O package:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the Gleam image (includes the Erlang runtime)
docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine

# Run the formatted output example (standard library only)
docker run --rm -v $(pwd):/work ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine sh -c "gleam new hello --skip-git > /dev/null 2>&1 && cp /work/io_output.gleam hello/src/hello.gleam && cd hello && gleam run"

# Run the file read/write example
docker run --rm -v $(pwd):/work ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine sh -c "gleam new hello --skip-git > /dev/null 2>&1 && cp /work/io_files.gleam hello/src/hello.gleam && cd hello && gleam add simplifile > /dev/null 2>&1 && gleam run"

# Run the error-handling example
docker run --rm -v $(pwd):/work ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine sh -c "gleam new hello --skip-git > /dev/null 2>&1 && cp /work/io_errors.gleam hello/src/hello.gleam && cd hello && gleam add simplifile > /dev/null 2>&1 && gleam run"

Each command creates a project called hello, copies your source into src/hello.gleam (the module gleam run executes), and compiles and runs it.

Expected Output

Running io_output.gleam (the println_error line goes to standard error, so it is not part of the standard output shown here):

Standard output, one line at a time.
No newline here... continued on the same line.
Name: Ada, Age: 36
Pi is approximately 3.14159
Half of 3 is 1.5
====================
DONE

Running io_files.gleam:

--- notes.txt ---
First line
Second line
Appended line
--- languages.txt ---
0: Gleam
1: Erlang
2: Elixir

Running io_errors.gleam:

Read present.txt: hello
Could not read missing.txt: No such file or directory.

Key Concepts

  • No printf — build strings — Gleam produces formatted output by concatenating with <> and converting values explicitly (int.to_string, float.to_string), so every value’s type is checked as you assemble the line.
  • print vs printlnio.print writes without a trailing newline while io.println adds one; both have _error variants (print_error, println_error) that write to standard error.
  • Separate numeric operators/. divides floats and / divides integers; the types never mix silently, which is why float and int conversions come from different modules.
  • File I/O is a packagesimplifile (added with gleam add simplifile) provides read, write, and append, using labelled arguments (from:, to:, contents:) that make calls read naturally.
  • Failures are values, not exceptions — every fallible I/O call returns a Result; there is no try/catch, and the compiler refuses to build a case on a Result unless both Ok and Error are handled.
  • describe_error for messagessimplifile.describe_error turns a FileError into a readable string, so you can report why an operation failed without pattern matching on every variant.
  • Pipelines around I/O — the effect itself is a single call, but the data around it stays functional: string.split, list.index_map, and list.each process file contents in a readable top-to-bottom flow.

Running Today

All examples can be run using Docker:

docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining