Intermediate

I/O Operations in Dart

Learn console output, reading user input, file reading and writing, I/O error handling, and formatted output in Dart with Docker-ready examples

Input and output are how a program talks to the outside world — printing to the console, reading what a user types, and persisting data to files. In Hello World you met print(), but Dart’s I/O story goes much deeper through the dart:io library.

Dart is a multi-paradigm language with sound null safety, and both traits shape how you do I/O. The dart:io library gives you synchronous methods (great for scripts and learning) and asynchronous, Future-based methods (great for servers and apps that must stay responsive). Because reading a line of input might return null at end-of-stream, null safety forces you to handle the “nothing was read” case explicitly — the compiler won’t let you forget.

In this tutorial you’ll learn how to write to standard output and standard error, read text typed by a user, write and read files, handle I/O errors gracefully, and produce cleanly formatted output. Every example runs on the command line with the official dart:stable Docker image.

Note: print() and string interpolation come from dart:core and need no import. Everything involving stdout, stdin, and File requires import 'dart:io';.

Console Output Beyond print()

print() is convenient, but dart:io exposes the underlying stdout and stderr streams for finer control — writing without a trailing newline, joining collections, and separating normal output from error output.

Create a file named io_output.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:io';

void main() {
  // print() always appends a newline
  print('Standard output with print()');

  // stdout.write() does NOT append a newline
  stdout.write('No newline here... ');
  stdout.write('same line!\n');

  // writeln() writes its argument followed by a newline
  stdout.writeln('Written with writeln()');

  // stderr is a separate stream reserved for error/diagnostic output
  stderr.writeln('This goes to standard error');

  // writeAll() joins an iterable with a separator
  stdout.writeAll(['a', 'b', 'c'], '-');
  stdout.writeln();
}

Using stdout directly lets you build output piece by piece, while stderr keeps diagnostics out of your program’s real results — important when output is piped into another command.

Reading User Input

To read a line typed by the user, call stdin.readLineSync(). It returns a String? — nullable, because it yields null at end-of-input. Null safety means you must decide what happens when nothing is read.

Create a file named io_input.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import 'dart:io';

void main() {
  stdout.write('What is your name? ');
  String? name = stdin.readLineSync();

  stdout.write('How old are you? ');
  String? ageInput = stdin.readLineSync();

  // int.parse converts text to a number; fall back to '0' if input was null
  int age = int.parse(ageInput ?? '0');

  // The ?? operator supplies a default when name is null
  print('Hello, ${name ?? 'stranger'}!');
  print('Next year you will be ${age + 1}.');
}

The ?? (null-coalescing) operator is doing the heavy lifting: ageInput ?? '0' guarantees int.parse receives a non-null string, and name ?? 'stranger' guarantees a friendly greeting even at end-of-input.

Writing and Reading Files

File access goes through the File class. The ...Sync methods block until the operation completes, which keeps command-line examples simple and linear. writeAsStringSync creates (or overwrites) a file; passing FileMode.append adds to it instead.

Create a file named io_files.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 'dart:io';

void main() {
  final file = File('notes.txt');

  // Write text — creates the file, or overwrites it if it already exists
  file.writeAsStringSync('First line\nSecond line\n');

  // Append more text without erasing what's there
  file.writeAsStringSync('Third line\n', mode: FileMode.append);

  // Read the whole file back as one string
  String contents = file.readAsStringSync();
  print('--- File contents ---');
  stdout.write(contents);

  // Read the file as a list of lines
  List<String> lines = file.readAsLinesSync();
  print('The file has ${lines.length} lines.');

  // Clean up after ourselves
  file.deleteSync();
  print('File deleted: ${!file.existsSync()}');
}

readAsStringSync gives you the raw contents; readAsLinesSync splits on line boundaries and hands back a List<String>, which is perfect when you want to process a file line by line.

Handling I/O Errors

File operations can fail — a path may not exist, or you may lack permission. Reading a missing file throws a PathNotFoundException (a FileSystemException subtype). Wrap risky calls in try/catch, or check first with existsSync().

Create a file named io_errors.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'dart:io';

void main() {
  final file = File('does_not_exist.txt');

  try {
    String contents = file.readAsStringSync();
    print(contents);
  } on PathNotFoundException catch (e) {
    // Catch the specific failure we expect
    print('Could not read file: ${e.path}');
  } catch (e) {
    // Catch anything else
    print('Unexpected error: $e');
  }

  // Alternatively, check before acting
  if (file.existsSync()) {
    print('File exists.');
  } else {
    print('File does not exist, skipping read.');
  }
}

The on PathNotFoundException clause catches the specific error, while the bare catch clause acts as a safety net. Checking existsSync() first is a good defensive habit when a missing file is an ordinary, expected situation rather than a true error.

Formatted Output

Dart formats output through methods on the values themselves rather than a printf-style format string. toStringAsFixed controls decimal places, padLeft/padRight align columns, and toRadixString converts numbers to other bases.

Create a file named io_format.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import 'dart:io';

void main() {
  double pi = 3.14159265;

  // Fixed number of decimal places
  print('Pi to 2 places: ${pi.toStringAsFixed(2)}');

  // Build an aligned table with padding
  List<List<String>> rows = [
    ['Apple', '3'],
    ['Banana', '12'],
    ['Cherry', '150'],
  ];

  for (var row in rows) {
    String name = row[0].padRight(10);  // left-align in 10 columns
    String qty = row[1].padLeft(5);     // right-align in 5 columns
    print('$name|$qty');
  }

  // Convert numbers to other bases and pad with zeros
  int value = 42;
  print('Hex: ${value.toRadixString(16)}');
  print('Binary: ${value.toRadixString(2)}');
  print('Padded: ${value.toString().padLeft(6, '0')}');
}

Because formatting lives on the values, you compose it naturally inside string interpolation — no separate format-string mini-language to memorize.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the official image
docker pull dart:stable

# Console output
docker run --rm -v $(pwd):/app -w /app dart:stable dart run io_output.dart

# Reading input — pipe two lines in via stdin (note the -i flag)
printf 'Alice\n30\n' | docker run --rm -i -v $(pwd):/app -w /app dart:stable dart run io_input.dart

# File writing and reading
docker run --rm -v $(pwd):/app -w /app dart:stable dart run io_files.dart

# I/O error handling
docker run --rm -v $(pwd):/app -w /app dart:stable dart run io_errors.dart

# Formatted output
docker run --rm -v $(pwd):/app -w /app dart:stable dart run io_format.dart

The -i flag on the input example keeps STDIN open so the piped values reach stdin.readLineSync().

Expected Output

Running io_output.dart:

Standard output with print()
No newline here... same line!
Written with writeln()
This goes to standard error
a-b-c

Running io_input.dart with Alice and 30 piped in:

What is your name? How old are you? Hello, Alice!
Next year you will be 31.

Running io_files.dart:

--- File contents ---
First line
Second line
Third line
The file has 3 lines.
File deleted: true

Running io_errors.dart:

Could not read file: does_not_exist.txt
File does not exist, skipping read.

Running io_format.dart:

Pi to 2 places: 3.14
Apple     |    3
Banana    |   12
Cherry    |  150
Hex: 2a
Binary: 101010
Padded: 000042

Key Concepts

  • dart:io is required for real I/Oprint() works out of the box, but stdout, stdin, and File all need import 'dart:io';.
  • stdout vs stderr — write results to stdout and diagnostics to stderr so output can be piped cleanly into other tools.
  • Input is nullablestdin.readLineSync() returns String?; sound null safety forces you to handle end-of-input, and the ?? operator makes supplying defaults concise.
  • Sync vs async — the ...Sync methods block and keep scripts linear; production servers use the Future-based versions with await to stay responsive.
  • FileMode.append — controls whether a write overwrites or appends; the default replaces the file’s contents.
  • Specific exceptions — catch PathNotFoundException (a FileSystemException) for missing files, and prefer existsSync() when a missing file is an expected, ordinary case.
  • Formatting lives on valuestoStringAsFixed, padLeft/padRight, and toRadixString compose naturally inside string interpolation instead of a printf format string.

Running Today

All examples can be run using Docker:

docker pull dart:stable
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining