Dart File Class
last modified May 30, 2026
The File class in Dart provides a rich API for working with files
on the filesystem. It supports reading, writing, copying, renaming, deleting,
and inspecting file metadata. The class is part of Dart's dart:io
library, which is available for server-side and command-line Dart applications.
File operations come in both asynchronous (Future-based) and
synchronous forms. Asynchronous methods are preferred in production
code because they do not block the event loop. For large files, Dart also
provides stream-based I/O via openRead() and
openWrite().
Basic Definition
A File object is a lightweight reference to a filesystem path.
Constructing one with File('path/to/file.txt') does not open or
create anything on disk; the actual I/O happens when you call a method such as
readAsString() or writeAsString().
Key capabilities of the File class:
- Reading and writing text or binary data
- Appending to existing files without overwriting them
- Accessing metadata: size, timestamps, and entity type
- Copying, renaming, and deleting files
- Streaming large files chunk by chunk
Reading a Text File
readAsString() reads the entire file content as a single
String. The optional encoding parameter defaults to
UTF-8. The method is asynchronous and returns a
Future<String>.
import 'dart:io';
void main() async {
final file = File('words.txt');
try {
final contents = await file.readAsString();
print(contents);
} on FileSystemException catch (e) {
print('Could not read file: ${e.message}');
}
}
A File object is created for words.txt. The
await keyword pauses execution until readAsString()
resolves, after which the full text is available in contents.
A FileSystemException is thrown when the file does not exist or
is not readable, so it is caught explicitly rather than using the
general Exception type.
$ dart read_text.dart sky blue warm falcon
Reading File Lines
readAsLines() splits the file on newlines and returns a
Future<List<String>>. Each element is one line
without the trailing newline character. This is convenient for processing
structured text files such as CSV or configuration files.
import 'dart:io';
void main() async {
final file = File('words.txt');
try {
final lines = await file.readAsLines();
print('Lines: ${lines.length}');
for (final (i, line) in lines.indexed) {
print('${i + 1}: $line');
}
} on FileSystemException catch (e) {
print('Could not read file: ${e.message}');
}
}
readAsLines() returns a List<String> where
each entry is a single line. The example uses Dart 3's
Iterable.indexed to get both the zero-based index and the line
value in the for loop, producing a numbered listing of every line.
$ dart read_lines.dart Lines: 4 1: sky 2: blue 3: warm 4: falcon
Writing to a File
writeAsString() writes a string to a file and returns a
Future<File>. The default FileMode.write
truncates and replaces any existing content. Pass FileMode.append
to add text to the end of the file without erasing it.
import 'dart:io';
void main() async {
final file = File('log.txt');
try {
// Overwrite the file with an initial header
await file.writeAsString('--- Application Log ---\n');
// Append individual log entries
await file.writeAsString('INFO app started\n', mode: FileMode.append);
await file.writeAsString('DEBUG loaded config\n', mode: FileMode.append);
await file.writeAsString('INFO ready\n', mode: FileMode.append);
print(await file.readAsString());
} on FileSystemException catch (e) {
print('File error: ${e.message}');
}
}
The first writeAsString() call uses the default
FileMode.write, which creates (or truncates) log.txt
and writes the header line. Each subsequent call uses
FileMode.append so the new lines accumulate rather than replacing
one another. The file is then read back to confirm the final result.
$ dart write_file.dart --- Application Log --- INFO app started DEBUG loaded config INFO ready
Writing Bytes
writeAsBytes() writes raw binary data from a
List<int> (commonly a Uint8List). The same
FileMode options apply. This is the correct method when working
with binary formats such as images, compressed archives, or custom binary
protocols.
import 'dart:io';
import 'dart:typed_data';
void main() async {
// Minimal valid GIF87a image (1x1 white pixel)
final gifBytes = Uint8List.fromList([
0x47, 0x49, 0x46, 0x38, 0x37, 0x61, // GIF87a signature
0x01, 0x00, 0x01, 0x00, 0x80, 0x00, // logical screen descriptor
0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, // global colour table
0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, // image descriptor
0x01, 0x00, 0x01, 0x00, 0x00, 0x02, // image data header
0x02, 0x44, 0x01, 0x00, 0x3B, // image data + trailer
]);
final file = File('pixel.gif');
await file.writeAsBytes(gifBytes);
print('Written ${gifBytes.length} bytes to ${file.path}');
// Read back and verify the GIF signature
final read = await file.readAsBytes();
final sig = String.fromCharCodes(read.sublist(0, 6));
print('Signature: $sig');
}
The Uint8List holds the raw bytes that form a minimal GIF image.
After writing, the file is read back and its 6-byte signature is decoded to
confirm the write succeeded. The same pattern applies whenever you need to
persist or verify a binary file format.
$ dart write_bytes.dart Written 35 bytes to pixel.gif Signature: GIF87a
Checking File Existence
exists() returns a Future<bool> that resolves
to true when the path refers to a file that is present on disk.
Use it as a guard before reading or writing to give users a clear error message
rather than a raw FileSystemException.
import 'dart:io';
Future<void> processFile(String path) async {
final file = File(path);
if (!await file.exists()) {
print('File not found: $path');
return;
}
final lines = await file.readAsLines();
print('Processing $path (${lines.length} lines)...');
for (final line in lines) {
print(' $line');
}
}
void main() async {
await processFile('words.txt');
await processFile('missing.txt');
}
processFile calls exists() first and returns early
with a descriptive message when the file is absent. Only when the file exists
does it proceed to read and process the lines. The second call demonstrates
the not-found path.
$ dart check_exists.dart Processing words.txt (4 lines)... sky blue warm falcon File not found: missing.txt
Reading File Metadata
The stat() method returns a Future<FileStat>
with information about the file: size in bytes, last-modified and
last-accessed timestamps, and the FileSystemEntityType
(file, directory, or link).
import 'dart:io';
void main() async {
final file = File('words.txt');
try {
final stat = await file.stat();
print('Type: ${stat.type}');
print('Size: ${stat.size} bytes');
print('Modified: ${stat.modified}');
print('Accessed: ${stat.accessed}');
print('Mode: ${stat.modeString()}');
} on FileSystemException catch (e) {
print('Could not stat file: ${e.message}');
}
}
FileStat.type distinguishes between a regular file, a directory,
a symbolic link, and other entity types. modeString() returns a
POSIX-style permission string (e.g. rw-r--r--) reflecting the
file's access permissions on the current platform.
$ dart file_stat.dart Type: file Size: 24 bytes Modified: 2025-04-04 10:30:45.000 Accessed: 2025-04-04 10:35:12.000 Mode: rw-r--r--
Copying, Renaming, and Deleting
copy(newPath) creates a duplicate of the file at the given path.
rename(newPath) moves or renames the file without making a copy —
when source and destination are on the same filesystem this is a cheap
metadata-only operation. delete() permanently removes the file.
import 'dart:io';
void main() async {
final original = File('words.txt');
// Copy to a backup
final backup = await original.copy('words.bak');
print('Copied → ${backup.path}');
// Rename the backup (original path no longer exists afterwards)
final renamed = await backup.rename('words_backup.txt');
print('Renamed → ${renamed.path}');
// Verify the renamed file exists, then delete it
if (await renamed.exists()) {
await renamed.delete();
print('Deleted ${renamed.path}');
}
}
copy() returns a new File reference pointing to the
copy. rename() returns a File for the new path —
the original path no longer exists afterwards. The existence check before
delete() is defensive; in production code you might instead catch
the FileSystemException that would be thrown if the file had
already been removed by another process.
$ dart manage_files.dart Copied → words.bak Renamed → words_backup.txt Deleted words_backup.txt
Reading Binary Data
readAsBytes() loads the entire file into a Uint8List.
The example below reads a PNG file and validates its 8-byte magic number, which
every valid PNG file starts with, and then extracts the image dimensions from
the IHDR chunk.
import 'dart:io';
// Every valid PNG file starts with these 8 bytes.
const List<int> pngMagic = [137, 80, 78, 71, 13, 10, 26, 10];
void main() async {
final file = File('image.png');
try {
final bytes = await file.readAsBytes();
print('Read ${bytes.length} bytes');
// Verify PNG magic number
final header = bytes.sublist(0, 8);
final isPng = List.generate(8, (i) => header[i] == pngMagic[i])
.every((match) => match);
print('Valid PNG: $isPng');
// IHDR chunk starts at offset 8; width and height are big-endian int32
int w = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
int h = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
print('Dimensions: ${w}x${h} px');
} on FileSystemException catch (e) {
print('Could not read file: ${e.message}');
}
}
The first 8 bytes are compared element-by-element against the known PNG magic number. If they match, the example also extracts the image width and height from the IHDR chunk, which begins at byte offset 16. Bit-shifting combines four bytes into a 32-bit big-endian integer — the format PNG uses for all multi-byte integers.
$ dart read_bytes.dart Read 4096 bytes Valid PNG: true Dimensions: 200x150 px
Reading Large Files with Streams
openRead() returns a Stream<List<int>>
that delivers the file in byte chunks. Piping it through
utf8.decoder and LineSplitter lets you process one
line at a time without loading the entire file into memory — essential for
large log files or data exports.
import 'dart:io';
import 'dart:convert';
void main() async {
final file = File('words.txt');
int lineCount = 0;
try {
final stream = file
.openRead()
.transform(utf8.decoder)
.transform(const LineSplitter());
await for (final line in stream) {
lineCount++;
print('$lineCount: $line');
}
print('Total lines: $lineCount');
} on FileSystemException catch (e) {
print('Could not open file: ${e.message}');
}
}
openRead() opens the file and starts streaming raw bytes.
utf8.decoder converts each byte chunk into a String.
LineSplitter reassembles the stream into complete lines, handling
the case where a chunk boundary falls in the middle of a line. The
await for loop processes each line as soon as it arrives, keeping
memory usage constant regardless of file size.
$ dart read_stream.dart 1: sky 2: blue 3: warm 4: falcon Total lines: 4
Best Practices
- Handle errors specifically: Catch
FileSystemExceptionrather than the genericExceptionso you can inspectosError.errorCodeand react appropriately (e.g. distinguish "not found" from "permission denied"). - Prefer async methods: Use the async variants
(
readAsString,writeAsString, …) to avoid blocking the event loop; reserve the sync equivalents for scripts and tests where simplicity matters more than concurrency. - Stream large files: Use
openRead()/openWrite()for files that may not fit comfortably in memory. - Use the
pathpackage: Build cross-platform paths withp.join()instead of string concatenation to avoid separator bugs on Windows. - Close IOSink explicitly: When using
openWrite(), callawait sink.flush()followed byawait sink.close()to ensure all buffered data is written and the file handle is released.
Source
This tutorial covered Dart's File class with practical examples
demonstrating reading and writing text and binary data, checking existence,
inspecting metadata, managing files with copy/rename/delete, and streaming
large files efficiently.
Author
List all Dart tutorials.