I/O #

dart:io is Dart’s built-in library for all input/output operations — reading and writing files, exploring directories, reading terminal input, running external processes, and working with networking. This library is only available on the Dart VM (server, CLI, desktop) — it can’t be used in the browser, which has a different I/O model. Understanding when to use synchronous vs asynchronous operations, and how to handle large files efficiently with streaming, is the core of reliable, responsive I/O.

Synchronous vs Asynchronous — a Choice That Matters #

dart:io provides two versions of almost every operation: synchronous (names ending in Sync) and asynchronous. Choosing the wrong one can make an app unresponsive or even crash.

import 'dart:io';

// SYNCHRONOUS — blocks the thread until done
String isiSync = File('data.txt').readAsStringSync(); // the thread stops here

// ASYNCHRONOUS — the thread is free to continue, the operation runs in the background
String isiAsync = await File('data.txt').readAsString(); // the thread doesn't stop
// ANTI-PATTERN: synchronous in a server or an app that needs responsiveness
// One slow request can block all other requests!
Future<Response> handleRequest(Request req) async {
  final data = File('large.csv').readAsStringSync(); // ✗ blocks the event loop!
  return Response.ok(data);
}

// CORRECT: always asynchronous for servers and responsive apps
Future<Response> handleRequest(Request req) async {
  final data = await File('large.csv').readAsString(); // ✓ non-blocking
  return Response.ok(data);
}
USE SYNCHRONOUS when:
  ✓ Simple CLI scripts running linearly from top to bottom
  ✓ Startup initialization (before the event loop starts)
  ✓ Code running on a separate Isolate (doesn't block the main isolate)

USE ASYNCHRONOUS when:
  ✓ Servers handling many requests
  ✓ UI apps that must stay responsive
  ✓ Almost every other case

File Operations #

Reading Files #

import 'dart:io';

// 1. Read the entire content as a String (for small-to-medium files)
final isi = await File('config.json').readAsString(encoding: utf8);

// 2. Read the entire content as bytes
final bytes = await File('gambar.png').readAsBytes();

// 3. Read line by line — already split by Dart
final baris = await File('data.csv').readAsLines();
for (final b in baris) {
  print(b);
}

Reading Large Files with Streaming #

For very large files (hundreds of MB or GB), reading everything into memory at once can cause an OutOfMemoryError. Use streaming:

import 'dart:io';
import 'dart:convert';

// Stream line by line — memory efficient for large files
Future<void> prosesFileBesar(String path) async {
  final file = File(path);

  // openRead() returns a Stream<List<int>> (bytes)
  // transform(utf8.decoder) → Stream<String> (text)
  // transform(LineSplitter()) → Stream<String> (per line)
  final stream = file
      .openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter());

  int nomorBaris = 0;
  await for (final baris in stream) {
    nomorBaris++;
    prosesSetiapBaris(baris, nomorBaris);
    // Only one line in memory at a time
  }

  print('Processed $nomorBaris lines');
}

// ANTI-PATTERN: reading a large file all at once
Future<void> prosesBuruk(String path) async {
  final semua = await File(path).readAsString(); // ✗ a 1GB file = 1GB in RAM
  for (final baris in semua.split('\n')) {
    prosesSetiapBaris(baris, 0);
  }
}

Writing Files #

import 'dart:io';

// 1. Write a String (overwrites the file if it exists)
await File('output.txt').writeAsString('New content');

// 2. Write bytes
await File('data.bin').writeAsBytes([0x48, 0x65, 0x6C, 0x6C, 0x6F]);

// 3. Write with FileMode
await File('log.txt').writeAsString(
  'New entry\n',
  mode: FileMode.append,   // append at the end, don't overwrite
  flush: true,              // flush to disk after writing
);

FileMode — Write Mode Control #

// Available modes:
FileMode.write    // write from the start, create if absent, overwrite if present (default)
FileMode.append   // append at the end of the file
FileMode.read     // read only
FileMode.writeOnly // write only, can't read
FileMode.writeOnlyAppend // append-write only

Streaming Writes for Large Data #

To write large data incrementally, use IOSink:

import 'dart:io';

Future<void> tulisLaporanBesar(String path, List<Transaksi> data) async {
  final sink = File(path).openWrite(mode: FileMode.write);

  try {
    // Write the header
    sink.writeln('ID,Tanggal,Jumlah,Keterangan');

    // Write data incrementally — no need for all of it in memory at once
    for (final t in data) {
      sink.writeln('${t.id},${t.tanggal},${t.jumlah},${t.keterangan}');
    }

    // Important: flush ensures all data is written to disk
    await sink.flush();
  } finally {
    // Always close the sink, even if there's an error
    await sink.close();
  }
}

Other File Operations #

final file = File('dokumen.txt');

// Check existence
bool ada = await file.exists();

// File info
FileStat stat = await file.stat();
print(stat.size);            // size in bytes
print(stat.modified);        // DateTime of last modification
print(stat.type);            // FileSystemEntityType

// Copy a file
await file.copy('dokumen_backup.txt');

// Rename / move
await file.rename('dokumen_baru.txt');

// Delete a file
await file.delete();

// Create an empty file if absent
await file.create(recursive: true); // recursive also creates parent directories

// File size
int ukuran = await file.length();

Directory Operations #

import 'dart:io';

// Current directory
Directory sekarang = Directory.current;
print(sekarang.path);

// System temp directory
Directory temp = await Directory.systemTemp.createTemp('dart_temp_');

final dir = Directory('data/laporan');

// Create a directory (recursive creates missing parents)
await dir.create(recursive: true);

// Check existence
bool ada = await dir.exists();

// Delete a directory (recursive also deletes its contents)
await dir.delete(recursive: true);

// Rename
await dir.rename('data/laporan_lama');

Directory Traversal #

import 'dart:io';

// List directory contents — one level
final dir = Directory('lib');
await for (final entitas in dir.list()) {
  if (entitas is File) {
    print('File: ${entitas.path}');
  } else if (entitas is Directory) {
    print('Dir:  ${entitas.path}');
  }
}

// Recursive listing — all contents including subdirectories
await for (final entitas in dir.list(recursive: true)) {
  print(entitas.path);
}

// Filter only .dart files
final dartFiles = dir
    .list(recursive: true)
    .where((e) => e is File && e.path.endsWith('.dart'))
    .cast<File>();

await for (final file in dartFiles) {
  print(file.path);
}

// Count the total directory size
int totalBytes = 0;
await for (final entitas in dir.list(recursive: true)) {
  if (entitas is File) {
    totalBytes += await entitas.length();
  }
}
print('Total: ${totalBytes ~/ 1024} KB');

Path Manipulation #

dart:io provides Platform.pathSeparator for cross-platform compatible paths, but the path package is far more complete:

dart pub add path
import 'package:path/path.dart' as p;

// Join paths — automatically uses the correct separator (\\ on Windows, / on Unix)
String fullPath = p.join('data', 'laporan', 'q4.csv');
// 'data/laporan/q4.csv' on Unix
// 'data\\laporan\\q4.csv' on Windows

// Path components
print(p.basename('/home/user/file.dart'));      // 'file.dart'
print(p.basenameWithoutExtension('/home/user/file.dart')); // 'file'
print(p.extension('/home/user/file.dart'));     // '.dart'
print(p.dirname('/home/user/file.dart'));       // '/home/user'

// Absolute vs relative paths
print(p.isAbsolute('/home/user'));   // true
print(p.isRelative('data/file'));    // true
print(p.absolute('data/file'));      // absolute path from the current directory

// Normalization
print(p.normalize('/home/user/../user/./file')); // '/home/user/file'

// Relative from one path to another
print(p.relative('/home/user/a', from: '/home/user/b')); // '../a'

Standard I/O — Terminal #

import 'dart:io';

// Write to stdout (without a trailing newline)
stdout.write('Enter your name: ');

// Read input from the keyboard (synchronous — fine for CLI)
String? input = stdin.readLineSync();
print('Hello, $input!');

// Write to stderr (for error messages)
stderr.writeln('Error: file not found');

// stdout/stderr as IOSink — support all IOSink methods
stdout.writeln('Message with newline');

// Read bytes from stdin (for binary data)
// stdin is a Stream<List<int>>
stdin.listen((data) {
  print('Data received: $data');
});

Interactive CLI #

import 'dart:io';

void main() {
  stdout.writeln('=== Simple Calculator ===');

  while (true) {
    stdout.write('Enter an expression (or "exit"): ');
    final input = stdin.readLineSync()?.trim();

    if (input == null || input == 'exit') {
      stdout.writeln('Goodbye!');
      break;
    }

    try {
      final hasil = hitungEkspresi(input);
      stdout.writeln('= $hasil');
    } on FormatException {
      stderr.writeln('Invalid format: $input');
    }
  }
}

Environment Variables and Platform #

import 'dart:io';

// Reading environment variables
String? path = Platform.environment['PATH'];
String? home = Platform.environment['HOME'];
String? javaHome = Platform.environment['JAVA_HOME'] ?? '/usr/lib/jvm/java';

// Platform info
print(Platform.operatingSystem);       // 'linux', 'macos', 'windows', 'android', 'ios'
print(Platform.operatingSystemVersion); // detailed OS version
print(Platform.localHostname);         // hostname
print(Platform.numberOfProcessors);    // number of CPU cores
print(Platform.pathSeparator);         // '/' or '\\'
print(Platform.isLinux);              // bool
print(Platform.isMacOS);              // bool
print(Platform.isWindows);            // bool

// Command-line arguments
// dart run script.dart arg1 arg2 --flag
print(Platform.executableArguments);  // arguments to the Dart VM
// Platform.script = URI of the running script
print(Platform.script.toFilePath()); // absolute script path

Running External Processes #

dart:io lets you run operating system commands through Process:

import 'dart:io';

// Run and wait for completion — capture all output
Future<void> contohProcess() async {
  // Way 1: run — wait for completion, capture output
  final result = await Process.run('ls', ['-la', '/tmp']);

  print('Exit code: ${result.exitCode}');
  print('stdout:\n${result.stdout}');
  if (result.stderr.isNotEmpty) {
    stderr.write(result.stderr);
  }

  // Way 2: start — stream output in real-time
  final process = await Process.start('tail', ['-f', 'server.log']);

  process.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen((baris) => print('[LOG] $baris'));

  process.stderr
      .transform(utf8.decoder)
      .listen((error) => stderr.write('[ERR] $error'));

  // Wait for the process to finish
  final exitCode = await process.exitCode;
  print('Process finished with code: $exitCode');
}

// Run in a shell (for piping, globs, etc.)
Future<String> jalankanShell(String perintah) async {
  final result = await Process.run(
    'bash', ['-c', perintah],
    runInShell: true,
  );
  if (result.exitCode != 0) {
    throw ProcessException('bash', ['-c', perintah],
        result.stderr.toString(), result.exitCode as int);
  }
  return result.stdout.toString().trim();
}

// Usage
void main() async {
  final versiGit = await jalankanShell('git --version');
  print(versiGit); // git version 2.x.x

  final filesCount = await jalankanShell('ls *.dart | wc -l');
  print('Number of Dart files: $filesCount');
}

File Watching — Monitoring Changes #

import 'dart:io';

// Watch file/directory changes in real-time
Future<void> pantauPerubahan(String path) async {
  final watcher = File(path).watch();

  await for (final event in watcher) {
    switch (event.type) {
      case FileSystemEvent.create:
        print('Created: ${event.path}');
      case FileSystemEvent.modify:
        print('Modified: ${event.path}');
      case FileSystemEvent.delete:
        print('Deleted: ${event.path}');
      case FileSystemEvent.move:
        final moveEvent = event as FileSystemMoveEvent;
        print('Moved: ${event.path}${moveEvent.destination}');
    }
  }
}

// Watch an entire directory
void pantauDirektori(String path) {
  Directory(path).watch(recursive: true).listen((event) {
    print('${event.type}: ${event.path}');
  });
}

I/O Anti-Patterns #

Not Closing Resources #

// ANTI-PATTERN: IOSink never closed — data may not be flushed
Future<void> tulisBuruk(String path) async {
  final sink = File(path).openWrite();
  sink.writeln('Important data');
  // ✗ the sink is never closed — data may be lost!
}

// CORRECT: always close with try-finally
Future<void> tulisBaik(String path) async {
  final sink = File(path).openWrite();
  try {
    sink.writeln('Important data');
    await sink.flush();
  } finally {
    await sink.close(); // ✓ always executed
  }
}

Not Checking File Existence #

// ANTI-PATTERN: reading directly without checking
Future<void> bacaBuruk(String path) async {
  final isi = await File(path).readAsString(); // ✗ FileSystemException if absent
  proses(isi);
}

// CORRECT: check first or catch the specific exception
Future<void> bacaBaik(String path) async {
  final file = File(path);
  if (!await file.exists()) {
    throw FileSystemException('File not found', path);
  }
  final isi = await file.readAsString();
  proses(isi);
}

// Or: use a specific try-on
Future<void> bacaDenganFallback(String path) async {
  try {
    final isi = await File(path).readAsString();
    proses(isi);
  } on FileSystemException catch (e) {
    print('Cannot read $path: ${e.message}');
    // use a default value or continue without the data
  }
}

Building Paths with String Concatenation #

// ANTI-PATTERN: string concatenation for paths — not cross-platform
String path = directoryPath + '/' + filename; // ✗ will be wrong on Windows

// CORRECT: use the path package
import 'package:path/path.dart' as p;
String path = p.join(directoryPath, filename); // ✓ correct on every platform

Summary #

  • dart:io is only available on the Dart VM — it can’t be used in code compiled to JavaScript (web). Separate I/O code from business logic so it’s easy to test and port.
  • Asynchronous is almost always better than synchronous — use readAsString(), not readAsStringSync(), except in simple CLI scripts or initialization code before the event loop.
  • Streaming for large filesopenRead() + transform(utf8.decoder) + transform(LineSplitter()) reads line by line without loading the entire file into memory.
  • FileMode.append for log files and growing data — FileMode.write (default) overwrites existing files.
  • Always close IOSink with finally — otherwise, unflushed data can be lost when the program ends.
  • Use the path package for path manipulation — string concatenation with / or \ isn’t portable across platforms.
  • Platform.environment for reading environment variables — a common pattern for app configuration that differs between development and production.
  • Process.run for short commands with captured output, Process.start for commands with real-time output streaming (like tail -f).
  • Catch FileSystemException specifically, not a generic catch (e) — so other errors aren’t masked and stay debuggable.
  • Directory.watch() for hot-reload and file watchers — useful for dev tools that need to detect file changes in real-time.

← Previous: Multithreading   Next: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact