skip to content

In Dart's dart:io, when should you read a file with File.openRead() rather than readAsString(), and how does File.openWrite() report errors?

level: middleimportance: should knowfreq 35%

answer

  1. whole file versus chunks
  2. Stream<List<int>> with optional offsets
  3. IOSink: write, flush, close
  4. FileMode.write truncates by default
  5. errors land on done, flush, close

basics

~10 s

Use File.openRead() for large files or incremental processing: it returns a Stream<List<int>> of byte chunks instead of loading everything. File.openWrite() returns an IOSink; write errors surface on its flush(), close() and done futures.

solid answer

~40 s

`readAsString()` needs memory for the whole file and gives you nothing until the end. `openRead([start, end])` returns a `Stream<List<int>>` of byte chunks, optionally limited to a byte range, which you decode with `utf8.decoder`, split with `LineSplitter` and consume with `await for` - memory stays flat. `openWrite()` returns an `IOSink`; its default `mode` is `FileMode.write`, which truncates the file, and `FileMode.append` writes at the end. `write()` only buffers, so you `await sink.flush()` and `await sink.close()`. If opening or writing fails, the `done`, `flush()` and `close()` futures all complete with a `FileSystemException`, and an unhandled error on `done` is an uncaught error - so await `close()` inside `try`/`catch`.

code

dart · 20 lines
dart
import 'dart:convert';
import 'dart:io';

Future<void> copyErrors(String logPath, String outPath) async {
  final lines = File(logPath)
      .openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter());

  final sink = File(outPath).openWrite(mode: FileMode.append);
  sink.done.ignore();
  try {
    await for (final line in lines) {
      if (line.contains('ERROR')) sink.writeln(line);
    }
    await sink.flush();
  } finally {
    await sink.close();
  }
}

go deeper

for a junior

Remember that openRead gives a stream of byte chunks and openWrite gives an IOSink you must close.

for a middle

Explain the decoder and LineSplitter chain, the truncate-versus-append modes, and that errors surface on done, flush and close rather than on write.

for a senior

Demonstrate memory-flat processing of large files and a write path whose failures are always observed, not silently dropped.

for a principal

Weigh whole-file convenience against streaming for the data sizes your tools really see, and standardise the sink-closing pattern.

## Two ways to read: whole or streamed In `dart:io`, a `File` can be read **all at once** or **as a stream**. The all-at-once methods - `readAsString()`, `readAsBytes()`, `readAsLines()` - return a Future that completes with the entire content. That is convenient, but the whole file must fit in memory and you get nothing until the last byte has arrived. `File.openRead([int? start, int? end])` returns a **`Stream<List<int>>`**: a sequence of byte chunks delivered as the file is read. The optional `start` and `end` offsets restrict it to a byte range, which is useful for resuming or sampling a large file. Use streaming when: - the file is **large** (logs, exports, media) and loading it whole would spike memory; - you want to **start processing early**, for example counting matching lines as they arrive; - you are **piping** the bytes elsewhere - into a socket, an HTTP response or another file - without needing the whole content. ## Turning bytes into lines The stream carries raw bytes, so text processing chains two transformers from `dart:convert`: `utf8.decoder` turns byte chunks into string chunks, and `LineSplitter` regroups those chunks into lines. You then consume the result with `await for`, and the file is closed automatically when the stream ends or the subscription is cancelled. ## Writing through an IOSink `File.openWrite({FileMode mode = FileMode.write, Encoding encoding = utf8})` returns an **`IOSink`**, the same sink type used for stdout, sockets and HTTP bodies. | Mode | Effect on an existing file | |---|---| | `FileMode.write` (default) | truncates to zero length, then writes | | `FileMode.append` | keeps content, writes at the end | Key behaviours of the sink: 1. `write()`, `writeln()` and `add()` **buffer** data; they return immediately and do not guarantee anything reached the disk. 2. `await sink.flush()` waits until buffered data has been handed to the file. 3. `await sink.close()` flushes, closes the file and frees the handle. The docs require closing the sink when you are done. 4. The sink does **not** translate `\n` into the platform line ending; write `Platform.lineTerminator` if you need `\r\n` on Windows. ## Where the errors go Because `write()` returns nothing, a failure - a directory path, a missing parent directory, no permission, a full disk - cannot be thrown at that line. Instead, per the `openWrite` documentation, the **`done`**, **`flush()`** and **`close()`** futures all complete with a `FileSystemException`. You must handle the error on `done`, or it becomes an **uncaught error**. The documented patterns are: - call `sink.done.ignore()` and then `await` both `flush()` and `close()` inside `try`/`catch`; or - attach a handler to `sink.done` with `catchError`. Forgetting to await `close()` has two costs: buffered data may never be written if the program ends first, and the error that explains why is silently lost. ## Common mistakes - Reading a multi-gigabyte file with `readAsString()` and wondering why memory spikes. - Treating `openRead()` as if it yields `String` lines directly - it yields bytes until you add decoders. - Calling `openWrite()` on an existing file expecting to append, then losing its content to the default truncating mode. - Firing `write()` calls without ever awaiting `close()`.

  • What do the start and end arguments of File.openRead() do?
    They limit the stream to a byte range of the file: reading starts at offset `start` and stops before `end`. That lets you resume a partially processed file or read only a header without streaming the rest.
  • Why does the openWrite() documentation insist you handle the done future?
    Errors opening or writing the file are reported on `done`, `flush()` and `close()`, not on the `write()` calls. If nobody listens to `done`, a failure becomes an uncaught asynchronous error. Ignoring `done` and awaiting `close()` in `try`/`catch` keeps the error where you can handle it.

saying these in an interview costs you the question

  • Thinks openWrite appends to an existing file by default
  • Expects write() to throw immediately when the disk or path is bad
  • Believes write() flushes to disk before returning
  • Assumes openRead() yields String lines without any decoding
  • Never awaits close() on the IOSink returned by openWrite