ZetCode

Dart read file

last modified January 28, 2024

In this article we show how to read files in Dart language. We read text and binary files.

The dart:io package is used to perform I/O operations, including reading files. It is available only in native Dart (CLI, server-side, Flutter mobile/desktop) — not in web/JavaScript targets. The File class contains methods for working with files and their contents.

Most File methods come in two flavours: an asynchronous version that returns a Future, and a synchronous version whose name ends in Sync. For server or CLI programs, prefer the async versions inside async functions to avoid blocking the event loop.

The try/catch keywords should be used to handle possible I/O exceptions such as FileSystemException.

Note that readAsString, readAsLines, and their synchronous counterparts load the entire file into memory at once. They are convenient for small files but should not be used for large files — use a Stream or RandomAccessFile instead.

words.txt
sky
blue
cup
cloud
water
war 
pen
dog

This is a simple text file.

Dart read file with readAsString

The readAsString method reads the entire file contents as a single string. The encoding defaults to UTF-8 but can be overridden via the encoding parameter (e.g. latin1, ascii). It returns a Future<String> that completes once the file has been fully read.

main.dart
import 'dart:io';

void main() async {
  var path = 'words.txt';

  try {
    var file = File(path);
    var contents = await file.readAsString();
    print(contents);
  } on FileSystemException catch (e) {
    stderr.writeln('Failed to read file: ${e.message}');
  }
}

We read a text file with readAsString and use try/catch to handle FileSystemException — the specific exception type thrown when an I/O operation fails.

import 'dart:io';

We import the dart:io package, which provides file I/O types.

var file = File(path);

A File object is created from a path string. At this point no actual I/O has occurred; the object is just a reference to the path.

var contents = await file.readAsString();

readAsString starts an asynchronous read. The await expression suspends the current function until the Future completes, then assigns the resulting string to contents.

} on FileSystemException catch (e) {
  stderr.writeln('Failed to read file: ${e.message}');
}

Catching FileSystemException specifically lets us access structured error information such as e.message, e.path, and e.osError.

$ dart main.dart 
sky
blue
cup
cloud
water
war 
pen
dog

Dart read file with readAsStringSync

The readAsStringSync method reads the entire file contents as a string synchronously. It blocks the calling isolate until the read completes and returns a String directly — there is no Future involved and no need for async/await. Use it only when blocking is acceptable, for example in simple command-line scripts or during startup initialisation.

main.dart
import 'dart:io';

void main() {
  var fileName = 'words.txt';

  try {
    var contents = File(fileName).readAsStringSync();
    print(contents);
  } on FileSystemException catch (e) {
    stderr.writeln('Error reading file: ${e.message} (${e.path})');
  }
}

The example reads a text file with readAsStringSync. The try/catch block guards against missing files or permission errors. Without it, an unhandled FileSystemException would terminate the program with a stack trace.

Dart check file existence

Before opening a file it is good practice to verify that it exists. The File.exists method returns a Future<bool>; existsSync is its synchronous counterpart. Both return true only when the path points to an existing regular file or symbolic link — not to a directory.

main.dart
import 'dart:io';

void main() async {
  var file = File('words.txt');

  if (await file.exists()) {
    var contents = await file.readAsString();
    print(contents);
  } else {
    stderr.writeln('File not found: ${file.path}');
  }
}

We check for existence with await file.exists() before reading. This avoids the FileSystemException that would otherwise be thrown for a missing file, and lets you handle the missing-file case with a clear message instead of a generic exception.

The synchronous variant works the same way without async/await:

main.dart
import 'dart:io';

void main() {
  var file = File('words.txt');

  if (file.existsSync()) {
    print(file.readAsStringSync());
  } else {
    stderr.writeln('File not found: ${file.path}');
  }
}

Note that checking existence and then reading are two separate system calls. In concurrent or multi-process environments the file could be deleted between the two calls (a time-of-check/time-of-use race). For maximum robustness, simply attempt the read inside a try/catch block and handle FileSystemException.

Dart file metadata with stat

File.stat returns a Future<FileStat> containing metadata about the file: its type, size in bytes, and last-modified timestamp. statSync is the synchronous equivalent. This information is useful for deciding how to read a file — for example, choosing a streaming approach when the file is large.

main.dart
import 'dart:io';

void main() async {
  var file = File('words.txt');
  var info = await file.stat();

  print('Type     : ${info.type}');
  print('Size     : ${info.size} bytes');
  print('Modified : ${info.modified}');
  print('Accessed : ${info.accessed}');
  print('Changed  : ${info.changed}');

  // Use size to pick an appropriate read strategy
  const largeFileThreshold = 10 * 1024 * 1024; // 10 MB
  if (info.size < largeFileThreshold) {
    var contents = await file.readAsString();
    print(contents);
  } else {
    print('File is too large to load into memory — use a Stream instead.');
  }
}

FileStat exposes the following fields:

$ dart main.dart
Type     : file
Size     : 38 bytes
Modified : 2024-01-28 10:15:03.000
Accessed : 2024-05-30 11:00:00.000
Changed  : 2024-01-28 10:15:03.000
sky
blue
…

Dart self read file

The next program reads its own source file. Platform.script returns the Uri of the script that was passed to the Dart VM, and toFilePath() converts it to an absolute file-system path. An explicit encoding: ascii argument is passed to demonstrate the optional encoding parameter; the default UTF-8 would work equally well for plain ASCII source files.

main.dart
import 'dart:convert';
import 'dart:io';

void main() async {
  var file = File(Platform.script.toFilePath());
  print(await (file.readAsString(encoding: ascii)));
}

We determine the program file path with Platform.script.toFilePath and read its contents with readAsString, passing the ascii codec from dart:convert as the encoding. The program prints its own source code to standard output.

$ dart main.dart
import 'dart:convert';
import 'dart:io';

void main() async {
  var file = File(Platform.script.toFilePath());
  print(await (file.readAsString(encoding: ascii)));
}

Dart read file with readAsLines

The readAsLines method reads the file asynchronously and splits the content into individual lines, stripping the line terminators. It returns a Future<List<String>>. Unlike readAsString, which gives you one big string, readAsLines gives you one element per line — convenient when you need to process or filter lines individually.

main.dart
import 'dart:io';

void main() {
  var fileName = 'words.txt';

  var myFile = File(fileName);

  myFile.readAsLines().then((lines) => lines.forEach((line) => print(line)));
}

The example reads a text file asynchronously line by line using a .then callback. When the Future completes, the callback receives the list of lines and iterates over them with forEach.

myFile.readAsLines().then((lines) => lines.forEach((line) => print(line)));

readAsLines returns a Future<List<String>>. The .then callback fires once all lines are available in memory.

In the next example, we use the cleaner async/await syntax, which is easier to read and compose, especially when additional processing is needed after reading.

main.dart
import 'dart:io';

void main() async {
  var fileName = 'words.txt';

  var myFile = File(fileName);
  var lines = await myFile.readAsLines();

  for (var line in lines) {
    print(line);
  }
}

The program reads the file with readAsLines using async/await. After the await expression, lines is a plain List<String> that can be indexed, filtered, or sorted like any other list.

Dart read file with readAsLinesSync

The readAsLinesSync method reads the file synchronously and returns a List<String> — one element per line, with line terminators stripped. It is the synchronous counterpart of readAsLines.

main.dart
import 'dart:io';

void main() {
  var fileName = 'words.txt';

  List<String> lines = File(fileName).readAsLinesSync();

  print('Line count: ${lines.length}');

  for (var i = 0; i < lines.length; i++) {
    print('${i + 1}: ${lines[i]}');
  }
}

The program reads a text file line by line in a synchronous way. Because the result is a plain list, we can access lines by index, measure its length, or apply standard collection methods such as where and map.

$ dart main.dart
Line count: 8
1: sky
2: blue
3: cup
4: cloud
5: water
6: war
7: pen
8: dog

Dart read file with Stream

File.openRead returns a Stream<List<int>> of raw bytes. This is the right approach for large files because the data flows through in chunks rather than being loaded all at once. The stream can be transformed with utf8.decoder (to convert bytes to strings) and LineSplitter (to split the decoded text into individual lines).

main.dart
import 'dart:convert';
import 'dart:io';

void main() {
  var fileName = 'words.txt';
  Stream<List<int>> stream = File(fileName).openRead();
  StringBuffer buffer = StringBuffer();

  stream
      .transform(utf8.decoder)
      .transform(LineSplitter())
      .listen(
        (line) => buffer.writeln(line),
        onDone: () => print(buffer.toString()),
        onError: (Object e) => stderr.writeln('Error: $e'),
      );
}

We read a text file using a Stream and accumulate the output in a StringBuffer.

Stream<List<int>> stream = File(fileName).openRead();

openRead opens the file and returns a stream that emits chunks of raw bytes. The optional start and end parameters can be used to read only a byte range.

StringBuffer buffer = StringBuffer();

StringBuffer provides efficient string concatenation. Appending to it is O(1), whereas repeated string concatenation with + would be O(n²).

stream
    .transform(utf8.decoder)
    .transform(LineSplitter())
    .listen(…);

The byte stream is first decoded from UTF-8 into a Stream<String>, then split into individual lines by LineSplitter. The listen call subscribes to the resulting line stream and provides three callbacks: one for each data event, one when the stream ends (onDone), and one for errors (onError).

An alternative — and often cleaner — approach is to use await for, which reads each line as it arrives without the need for explicit callbacks.

main.dart
import 'dart:convert';
import 'dart:io';

void main() async {
  var fileName = 'words.txt';

  var lineStream = File(fileName)
      .openRead()
      .transform(utf8.decoder)
      .transform(LineSplitter());

  try {
    await for (var line in lineStream) {
      print(line);
    }
  } on FileSystemException catch (e) {
    stderr.writeln('Error: ${e.message}');
  }
}

The await for loop iterates over the line stream one line at a time, suspending the function at each iteration until the next event arrives. This style is particularly readable and works naturally with the surrounding try/catch for error handling.

Dart read binary file

The readAsBytes method reads the entire file contents as a Uint8List (a typed list of unsigned 8-bit integers). It returns a Future<Uint8List>. This is the correct method when working with binary data such as images, archives, or any file whose content is not plain text.

main.dart
import 'dart:convert';
import 'dart:io';

void main() async {
  var fileName = 'favicon.ico';

  try {
    var bytes = await File(fileName).readAsBytes();

    print('File size: ${bytes.length} bytes');

    // Encode entire file as Base64
    var base64String = base64.encode(bytes);
    print('Base64 (first 60 chars): ${base64String.substring(0, 60)}...');

    // Print first 16 bytes as two-digit hex values
    var hex = bytes
        .take(16)
        .map((b) => b.toRadixString(16).padLeft(2, '0'))
        .join(' ');
    print('First 16 bytes: $hex');
  } on FileSystemException catch (e) {
    stderr.writeln('Error: ${e.message}');
  }
}

We read a small binary file (a favicon) and inspect its contents in two ways.

var bytes = await File(fileName).readAsBytes();

readAsBytes reads the file into a Uint8List. Because Uint8List implements List<int>, all standard collection methods — map, where, take, etc. — work directly on it.

var base64String = base64.encode(bytes);

base64.encode from dart:convert encodes the raw bytes as a Base64 string, which is useful when embedding binary data in JSON or HTML.

var hex = bytes
    .take(16)
    .map((b) => b.toRadixString(16).padLeft(2, '0'))
    .join(' ');

We take the first 16 bytes and convert each to a two-digit hexadecimal string with toRadixString(16) and padLeft(2, '0'), producing output like 00 00 01 00 01 00 10 10 …. This is helpful for inspecting file magic bytes or binary structures.

Dart read binary file with readAsBytesSync

The readAsBytesSync method is the synchronous counterpart of readAsBytes. It blocks until the file has been read and returns a Uint8List directly.

main.dart
import 'dart:io';

void main() {
  var fileName = 'favicon.ico';

  try {
    var bytes = File(fileName).readAsBytesSync();
    print('File size: ${bytes.length} bytes');

    // Check PNG magic bytes: 89 50 4E 47
    if (bytes.length >= 4 &&
        bytes[0] == 0x89 &&
        bytes[1] == 0x50 &&
        bytes[2] == 0x4E &&
        bytes[3] == 0x47) {
      print('File is a PNG image');
    } else {
      print('File is not a PNG image');
    }
  } on FileSystemException catch (e) {
    stderr.writeln('Error: ${e.message}');
  }
}

The example reads a binary file synchronously and checks its magic bytes to identify the file type. Many binary formats can be identified by their first few bytes: PNG files start with 89 50 4E 47, JPEG files with FF D8 FF, and ZIP files with 50 4B 03 04.

Dart read file with RandomAccessFile

File.open returns a Future<RandomAccessFile> which lets you seek to arbitrary positions and read a specific number of bytes. This is useful for large binary files, fixed-width record formats, or any situation where you want to read only part of a file without loading it all into memory.

main.dart
import 'dart:io';

void main() async {
  var fileName = 'words.txt';
  RandomAccessFile? raf;

  try {
    raf = await File(fileName).open();

    var length = await raf.length();
    print('File length: $length bytes');

    // Read the first 10 bytes
    var firstChunk = await raf.read(10);
    print('First 10 bytes as string: "${String.fromCharCodes(firstChunk)}"');

    // Seek back to the start and read 4 bytes again
    await raf.setPosition(0);
    var header = await raf.read(4);
    print('First 4 bytes: $header');

    // Jump to a specific offset
    await raf.setPosition(length - 4);
    var tail = await raf.read(4);
    print('Last 4 bytes: $tail');
  } on FileSystemException catch (e) {
    stderr.writeln('Error: ${e.message}');
  } finally {
    await raf?.close();
  }
}

We open the file as a RandomAccessFile and perform several positioned reads.

raf = await File(fileName).open();

File.open opens the file for random access. The default mode is FileMode.read. The RandomAccessFile must be closed explicitly — the finally block guarantees this even if an exception is thrown.

var firstChunk = await raf.read(10);

read(count) reads up to count bytes starting at the current file position and advances the position by the number of bytes actually read.

await raf.setPosition(0);

setPosition moves the read/write pointer to any byte offset in the file. This is the key feature that distinguishes RandomAccessFile from a sequential stream.

await raf?.close();

The null-aware ?.close() closes the file handle safely even if the open call failed and raf is still null. Always close RandomAccessFile instances to release OS file descriptors.

Dart chunked reading with readInto

RandomAccessFile.readInto reads bytes into a pre-allocated buffer and returns the number of bytes actually read. Because the same buffer object is reused on every iteration, memory usage stays constant regardless of file size. This is the canonical pattern for processing large binary files efficiently.

main.dart
import 'dart:io';

void main() async {
  const chunkSize = 4096;
  var fileName = 'large.bin';
  RandomAccessFile? raf;

  try {
    raf = await File(fileName).open();

    var buffer = List<int>.filled(chunkSize, 0);
    var totalBytes = 0;

    while (true) {
      var bytesRead = await raf.readInto(buffer);
      if (bytesRead == 0) break;

      totalBytes += bytesRead;

      // Process only the valid portion of the buffer
      var chunk = buffer.sublist(0, bytesRead);
      // ... process chunk ...
      stdout.write('Read $bytesRead bytes (total: $totalBytes)\r');
    }

    print('\nDone. Total bytes read: $totalBytes');
  } on FileSystemException catch (e) {
    stderr.writeln('Error: ${e.message}');
  } finally {
    await raf?.close();
  }
}

We open the file with File.open and allocate a fixed-size buffer once before the loop.

var bytesRead = await raf.readInto(buffer);
if (bytesRead == 0) break;

readInto(buffer) fills buffer from the current file position and returns how many bytes were written. A return value of 0 means the end of the file has been reached — the loop exits.

var chunk = buffer.sublist(0, bytesRead);

On the last iteration, fewer bytes than chunkSize may be available. Using sublist(0, bytesRead) ensures only the valid bytes are processed and the padding zeros at the tail of the buffer are ignored.

Dart directory iteration

When you need to read multiple files from a folder, use Directory.list, which returns a Stream of FileSystemEntity objects. Entities can be File, Directory, or Link instances. Use the is type check to select only files, and filter by extension as needed.

main.dart
import 'dart:io';

void main() async {
  var dir = Directory('data');

  if (!await dir.exists()) {
    stderr.writeln('Directory not found: ${dir.path}');
    return;
  }

  await for (var entity in dir.list(recursive: false)) {
    if (entity is File && entity.path.endsWith('.txt')) {
      print('--- ${entity.path} ---');
      print(await entity.readAsString());
    }
  }
}

We iterate over all entries in the data directory and print the content of every .txt file found.

await for (var entity in dir.list(recursive: false)) {

Directory.list returns a Stream<FileSystemEntity>. Setting recursive: true descends into sub-directories. The await for loop processes one entry at a time as they arrive from the stream.

if (entity is File && entity.path.endsWith('.txt')) {

The is File check narrows the type from the base FileSystemEntity to File, enabling methods such as readAsString. The path check filters out non-text files.

If you only need to list the paths without reading the contents, use dir.listSync() for a simpler synchronous approach:

main.dart
import 'dart:io';

void main() {
  var entries = Directory('data')
      .listSync(recursive: true)
      .whereType<File>()
      .where((f) => f.path.endsWith('.txt'))
      .toList();

  print('Found ${entries.length} text file(s):');
  for (var f in entries) {
    print('  ${f.path}  (${f.statSync().size} bytes)');
  }
}

listSync returns a List<FileSystemEntity>. whereType<File> filters and narrows the type in one step, replacing the manual is File check. statSync().size retrieves the file size without a separate stat call.

Dart platform paths

Path separators differ between operating systems: / on Linux and macOS, \ on Windows. Hard-coding separators makes code non-portable. The Platform class from dart:io provides OS detection, while the path package offers portable path manipulation.

main.dart
import 'dart:io';

void main() {
  print('OS        : ${Platform.operatingSystem}');
  print('Separator : "${Platform.pathSeparator}"');
  print('Script    : ${Platform.script.toFilePath()}');
  print('Exe       : ${Platform.executable}');
  print('Is Linux  : ${Platform.isLinux}');
  print('Is macOS  : ${Platform.isMacOS}');
  print('Is Windows: ${Platform.isWindows}');

  // Build a cross-platform path manually (works, but verbose)
  var sep = Platform.pathSeparator;
  var manualPath = ['data', 'subdir', 'words.txt'].join(sep);
  print('Manual path: $manualPath');
}

Platform.operatingSystem returns a lowercase string such as "linux", "macos", or "windows". The boolean getters Platform.isLinux etc. are more concise for conditional logic.

For real path manipulation use the path package (package:path/path.dart), which handles separators, normalization, and relative/absolute conversions automatically:

main.dart
import 'dart:io';
import 'package:path/path.dart' as p;

void main() async {
  // Build a portable path from components
  var filePath = p.join('data', 'subdir', 'words.txt');
  print('Joined   : $filePath');
  print('Basename : ${p.basename(filePath)}');
  print('Dirname  : ${p.dirname(filePath)}');
  print('Extension: ${p.extension(filePath)}');

  // Absolute path of the current working directory
  var cwd = Directory.current.path;
  var absolute = p.absolute(cwd, filePath);
  print('Absolute : $absolute');

  // Read the file using the portable path
  var file = File(filePath);
  if (await file.exists()) {
    print(await file.readAsString());
  }
}

Add the package to pubspec.yaml before using it:

dependencies:
  path: ^1.9.0

Key functions from package:path:

Dart file reading method comparison

The table below summarises the available methods and helps you choose the right one for each situation.

Method Return type Best for Memory usage
readAsString Future<String> Small text files, configuration Entire file loaded
readAsStringSync String Scripts, startup init (blocking OK) Entire file loaded
readAsLines Future<List<String>> Line-oriented text, CSV, logs (small/medium) Entire file loaded
readAsLinesSync List<String> Same as above, synchronous variant Entire file loaded
openRead + LineSplitter Stream<String> Large text files, real-time log tailing Constant (chunk-based)
readAsBytes Future<Uint8List> Small binary files (images, archives) Entire file loaded
readAsBytesSync Uint8List Same as above, synchronous variant Entire file loaded
RandomAccessFile.read Future<Uint8List> Seek + read at specific offsets, binary formats Constant (reads N bytes)
RandomAccessFile.readInto Future<int> Large binary files, streaming with reusable buffer Constant (buffer reused)

Source

Dart I/O documentation

In this article we have shown how to read files in Dart using readAsString, readAsLines, readAsBytes, Stream, RandomAccessFile, directory iteration, file existence checks, metadata via stat, and platform-portable paths.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.