I/O Operations in Clojure
Learn console and file I/O in Clojure - printing, reading input, slurp/spit, line-by-line reading, and I/O error handling with Docker-ready examples
Input and output are how a program talks to the outside world: printing results, reading what a user types, and persisting data to files. In Hello World you saw println, but that is only the beginning of what Clojure offers.
Because Clojure is a functional language hosted on the JVM, its approach to I/O has two distinct flavors. On one hand, Clojure provides beautifully terse functions like slurp and spit that read or write an entire file in a single expression. On the other hand, I/O is inherently a side effect — it is not pure — so Clojure keeps these operations explicit and often pairs them with with-open to manage resources safely and try/catch to handle failures.
This tutorial covers writing to the console (beyond println), reading from standard input, reading and writing files, processing files line-by-line with lazy sequences, and handling I/O errors gracefully. Every example is a self-contained script you can run directly with the Clojure Docker image.
Console Output Beyond println
Clojure offers several printing functions, each with a purpose. println and print produce human-readable text, while prn and pr produce machine-readable (round-trippable) output where strings keep their quotes. For structured formatting, format builds a string with Java-style format specifiers.
Create a file named output.clj:
| |
The distinction between println and prn matters: println is for people, prn is for data you might read back into Clojure later.
Reading Console Input
The read-line function reads a single line from standard input and returns it as a string (or nil at end-of-file). To keep this example reproducible when run non-interactively, we use with-in-str, which temporarily binds standard input to a string. In a real interactive program you would call read-line directly.
Create a file named input.clj:
| |
Note the flush in the commented example: print is buffered, so without flushing, an interactive prompt might not appear before the program waits for input.
Writing Files with spit
Clojure’s spit writes a string to a file in one call, creating the file or overwriting it. Passing :append true adds to an existing file instead. This terseness is idiomatic Clojure — common operations should be one expression.
Create a file named file_write.clj:
| |
Notice how slurp is the mirror image of spit: it reads an entire file into a single string.
Reading Files Line by Line
While slurp is perfect for small files, large files are better processed lazily one line at a time. Clojure’s line-seq returns a lazy sequence of lines from a reader, and with-open guarantees the reader is closed afterward — even if an exception is thrown mid-way. This is the functional, resource-safe way to stream data.
Create a file named file_read.clj:
| |
Because line-seq is lazy, you can pipe it through map, filter, reduce, and other sequence functions without loading the whole file into memory.
Handling I/O Errors
I/O can fail: files go missing, disks fill up, permissions are wrong. Clojure uses the JVM’s exception mechanism through try/catch/finally. You can catch specific Java exception classes and provide a fallback, and finally runs cleanup code regardless of success or failure.
Create a file named io_errors.clj:
| |
Catching a narrow exception type (FileNotFoundException) before the broad one (Exception) lets you give precise, useful error messages while still having a catch-all safety net.
Running with Docker
You can run every example with the official Clojure image — no local install required.
| |
The -v $(pwd):/app volume mount means files written by spit (like greetings.txt and poem.txt) appear in your current directory after the container exits.
Expected Output
Running output.clj:
Written with println
No newline -> same line
"quoted"
[1 2 3]
Sum of 2 and 3 is 5
Name: Ada, Age: 36
Pi is about 3.14
abc123
Running input.clj:
Hello, Grace!
Next year you will be 43.
Running file_write.clj:
Files written.
Hello from Clojure!
A second line.
---
Alpha
Beta
Gamma
Running file_read.clj:
Whole file with slurp:
roses are red
violets are blue
Clojure is
functional too
Line by line:
> roses are red
> violets are blue
> Clojure is
> functional too
Total lines: 4
Running io_errors.clj:
important data
[missing file: does-not-exist.txt]
Working with resources...
Cleanup always runs.
Key Concepts
slurpandspitare the workhorses — read or write an entire file in a single expression;spittakes:append trueto add rather than overwrite.- Choose your print function by intent —
println/printfor humans,prn/prfor machine-readable data that round-trips back into Clojure, andformatfor structured, specifier-based output. - I/O is a side effect — unlike Clojure’s pure functions, I/O interacts with the outside world, so these operations are kept explicit and are ordered by evaluation.
with-openguarantees cleanup — it closes readers and writers automatically, even when an exception is thrown, making resource leaks nearly impossible.line-seqis lazy — process arbitrarily large files line-by-line, composing withmap,filter, andreducewithout loading everything into memory.read-linereturns a string ornil— remember toflushafter aprint-based prompt so it appears before input is read.- Error handling uses JVM exceptions —
try/catch/finallylets you catch specific classes likejava.io.FileNotFoundExceptionand provide fallbacks, withfinallyfor cleanup that always runs.
Comments
Loading comments...
Leave a Comment