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.
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.
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.
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.
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:
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.
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:
type— aFileSystemEntityTypevalue:file,directory,link, ornotFound.size— file size in bytes (always 0 for directories on some platforms).modified— aDateTimeof the last write.accessed— aDateTimeof the last read access (not updated on all OSes).changed— aDateTimewhen the inode metadata last changed (Unix only).
$ 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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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:
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.
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:
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:
p.join(parts…)— assembles a path from components using the platform separator.p.basename(path)— returns the final path component (filename).p.dirname(path)— returns everything except the final component.p.extension(path)— returns the file extension including the dot, e.g..txt.p.absolute(…)— resolves a relative path against a base to produce an absolute 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
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
List all Dart tutorials.