skip to content

What goes wrong when you return a lazy `Sequence` (e.g. from `lineSequence()`) out of a `use` block, and how do you consume file lines safely?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Sequence is lazy; use closes early
  2. returning lineSequence() -> Stream closed
  3. terminate inside the block (toList/forEach/count)
  4. prefer File.useLines for streaming
  5. useLines also bug if you return the bare Sequence

basics

~20 s

A lazy sequence only reads when you iterate it. If you return it from use, the reader is already closed before you read, so iteration fails. Consume the lines inside the block, or use useLines.

solid answer

~40 s

`use` closes the resource the moment its block returns. A `Sequence` from `lineSequence()` (or `Reader.buffered().lineSequence()`) is **lazy**: it does not read the file until iterated. So `reader.use { it.lineSequence() }` returns a sequence backed by an already-closed reader; iterating it later throws (e.g. `IOException: Stream closed`). The fix is to **terminate** the sequence inside the block — `it.lineSequence().toList()` / `.count()` / `.forEach { }` — so all reads happen before close, or to use the dedicated `File.useLines { lines -> ... }` extension, which opens a reader, hands you the lazy `Sequence<String>`, and closes it in a finally after your block consumes it. For huge files prefer `useLines` with a streaming terminal operation over `readLines()`/`toList()` to avoid loading every line into memory.

code

kotlin · 7 lines
kotlin
// streams line-by-line, constant memory, reader closed after block
val matches = File("app.log").useLines { lines ->
    lines.filter { it.startsWith("WARN") }.count()
}

// WRONG: bare sequence escapes the block -> reader already closed on iteration
// val s = File("app.log").useLines { it } // do not do this

go deeper

for a junior

May not see the laziness trap; can read a whole file with readText()/readLines().

for a middle

Knows to terminate the sequence inside the block and that readLines() materialises everything.

for a senior

Explains lazy-sequence-vs-close interaction, reaches for useLines, and reasons about streaming vs memory.

for a principal

Sets guidance for large-file processing (streaming with useLines, backpressure/memory budgets) and flags leaked-lazy-handle patterns.

## The trap: laziness crosses the close boundary A **`Sequence`** in Kotlin is lazy — intermediate operations (`map`, `filter`, `lineSequence`) do nothing until a **terminal** operation (`toList`, `count`, `forEach`, `first`) pulls elements. `lineSequence()` reads each line from the underlying reader only when the consumer asks for it. `use` closes the reader as soon as its block returns. So: ```kotlin // BUG val lines: Sequence<String> = File("big.txt").bufferedReader().use { it.lineSequence() } lines.forEach(::println) // throws: the reader was closed when `use` returned ``` The sequence is handed back, but its data source is gone. ## Fix 1: terminate inside the block Force all reads to happen before close by adding a terminal operation **inside** the `use`: ```kotlin val lines: List<String> = File("big.txt").bufferedReader().use { it.readLines() } // or: use { it.lineSequence().toList() } ``` Now every line is read while the reader is open; `use` then closes it. ## Fix 2: `useLines` (preferred for streaming) The stdlib provides `File.useLines` (and `Reader.useLines`) for exactly this. It opens a reader, gives your block a lazy `Sequence<String>`, and closes the reader in a finally **after** your block finishes — so you can stream without materialising everything: ```kotlin val errorCount = File("big.txt").useLines { lines -> lines.count { it.contains("ERROR") } // terminal op runs while reader is open } ``` Signature shape: ```kotlin public inline fun <T> File.useLines( charset: Charset = Charsets.UTF_8, block: (Sequence<String>) -> T ): T ``` The key rule: the **terminal operation must run inside `block`**. Returning the bare `Sequence` out of `useLines` has the same closed-reader bug. ## Memory note `readLines()`/`lineSequence().toList()` load **all** lines into memory. For large files, `useLines { it.forEach { ... } }` or `useLines { it.count { ... } }` streams line-by-line with constant memory. Choose based on file size. ## Same principle generalises Any lazy iterator/stream (a `Stream`, a DB cursor) backed by a `use`-closed resource must be fully consumed inside the block; never leak the lazy handle past close.

  • Why does iterating the returned sequence throw 'Stream closed'?
    The sequence is lazy and only reads on iteration, but `use`/`useLines` already closed the underlying reader when the block returned.
  • When prefer `useLines` over `readLines()`?
    For large files: `useLines` with a streaming terminal op keeps constant memory, whereas `readLines()`/`toList()` materialises every line.

It's like handing someone a straw to a cup you immediately throw away — they go to sip and there's nothing there.

saying these in an interview costs you the question

  • Returns `lineSequence()`/a bare Sequence out of `use` or `useLines`
  • Thinks a Sequence eagerly reads at creation time
  • Uses `readLines()` on a multi-GB file and OOMs
  • Believes `useLines` keeps the reader open after the block
  • Adds the terminal operation outside the block

context