Watch the video (12 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. The File Class
  5. 2. Writing Text with writeAsString
  6. 3. Reading Text with readAsString
  7. 4. Writing Replaces the Old Content
  8. 5. Adding to a File with FileMode.append
  9. 6. The Synchronous Versions
  10. 7. Lists of Lines: join and readAsLines
  11. 8. exists and delete
  12. 9. A Missing File Throws
  13. 10. Catching FileSystemException
  14. 11. Writing Piece by Piece with openWrite
  15. 12. Reading a Large File Line by Line
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 106 — File Handling

Category18 — Files, JSON & Core Utilities
Lecture106 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelIntermediate

Every value in our programs disappears the moment main ends — Ali's marks, Sara's balance, all gone. A file keeps data on the disk, so the next run can read it back.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every output and error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2. Long stack traces are shortened to their first lines.


Prerequisites

Students should already understand, from Lectures 043–101:

  • join, split, map and reduce on lists, and map entries (Lectures 043–050)
  • try/catch and on SomeException catch (e) (Lecture 084)
  • Future, async/await, and try/catch around await (Lectures 088–090)
  • streams, await for and transform (Lectures 092 and 095)
  • importing core libraries such as dart:io (Lecture 101)

Connection With Previous Lectures

Here is Ali's mark list from many earlier lectures:

dart
void main() {
  var marks = [80, 75, 85];
  print(marks);
}
output
[80, 75, 85]

Run it again tomorrow and the list is rebuilt from the code — any mark added while the program ran is lost. Variables live in memory, and memory is cleared when the program stops. In Lecture 101 we saw that dart:io holds tools for files. Now we use them to make data outlive the program.


Learning Objectives

By the end of this lecture you will be able to:

  • Create a File object from a file name
  • Write and read text with writeAsString and readAsString
  • Add to a file with FileMode.append instead of replacing it
  • Use the synchronous versions writeAsStringSync and readAsStringSync
  • Save a list as lines and read it back with readAsLines
  • Check for a file with exists and remove it with delete
  • Catch a FileSystemException when a file is missing
  • Write piece by piece with openWrite, and read a large file line by line with openRead

1. The File Class

File comes from dart:io. File('notes.txt') creates an object that points to a file name. It does not touch the disk yet.

Analogy — a File object is like a label for a drawer in a cabinet. Writing the label does not put anything in the drawer; you still have to open it and put papers in or take them out.

dart
import 'dart:io';

void main() {
  var file = File('notes.txt');
  print(file.path);
  print(file);
}
output
notes.txt
File: 'notes.txt'

A plain name like 'notes.txt' is a relative path: the file lives in the folder you ran dart run from. We will study folders and full paths in Lecture 107.


2. Writing Text with writeAsString

Disk work is slow compared with memory, so file methods return a Future (Lecture 088). We await them:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('notes.txt');
  await file.writeAsString('Hello, Ali!');
  print('Saved to ${file.path}');
}
output
Saved to notes.txt

If notes.txt does not exist, writeAsString creates it. Open the folder and you will find the file with the text inside.


3. Reading Text with readAsString

readAsString returns a Future<String> — the whole file as one string:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('notes.txt');
  await file.writeAsString('Hello, Ali!');
  var text = await file.readAsString();
  print(text);
  print(text.length);
}
output
Hello, Ali!
11

Once read, text is an ordinary String, with length, split and everything else.


4. Writing Replaces the Old Content

By default, writeAsString overwrites the file:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('notes.txt');
  await file.writeAsString('First note');
  await file.writeAsString('Second note');
  print(await file.readAsString());
}
output
Second note

First note is gone. That is right for a settings file, but wrong for a log.


5. Adding to a File with FileMode.append

Pass mode: FileMode.append to add to the end instead:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('marks.txt');
  await file.writeAsString('Mon: 80\n');
  await file.writeAsString('Tue: 75\n', mode: FileMode.append);
  await file.writeAsString('Wed: 90\n', mode: FileMode.append);
  stdout.write(await file.readAsString());
}
output
Mon: 80
Tue: 75
Wed: 90

The first write has no mode, so it starts the file fresh. Append does not add a line break for you — the \n at the end of each string does. We use stdout.write (Lecture 101) because the text already ends with \n, and print would add an extra blank line.


6. The Synchronous Versions

Each method has a Sync twin that returns the value directly and needs no await:

dart
import 'dart:io';

void main() {
  var file = File('settings.txt');
  file.writeAsStringSync('theme=dark');
  var text = file.readAsStringSync();
  print(text);
}
output
theme=dark

A Sync call blocks: nothing else runs until the disk finishes (Lecture 087). That is fine for a small script; in an app that must stay responsive, prefer the async versions.


7. Lists of Lines: join and readAsLines

To save a list, join it with \n. To get it back, readAsLines splits the file into a List<String>:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('students.txt');
  var names = ['Ali', 'Sara', 'Zayan'];
  await file.writeAsString(names.join('\n'));

  var lines = await file.readAsLines();
  print(lines);
  print(lines.length);
  print(lines[1]);
}
output
[Ali, Sara, Zayan]
3
Sara

A file that ends with \n does not produce an empty last line, so readAsLines works with both styles.


8. exists and delete

exists() answers whether the file is there; delete() removes it. Both return futures:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('temp.txt');
  print(await file.exists());
  await file.writeAsString('temporary');
  print(await file.exists());
  await file.delete();
  print(await file.exists());
}
output
false
true
false

9. A Missing File Throws

Reading a file that does not exist is an exception, not a bug in your code — the user may have deleted it:

dart
import 'dart:io';

Future<void> main() async {
  var text = await File('missing.txt').readAsString();
  print(text);
}
output
Unhandled exception:
PathNotFoundException: Cannot open file, path = 'missing.txt' (OS Error: No such file or directory, errno = 2)
#0      _checkForErrorResponse (dart:io/common.dart:58:9)

The program crashes. PathNotFoundException is one kind of FileSystemException, the type for every file problem: missing file, no permission, disk full.


10. Catching FileSystemException

Exactly as in Lecture 084, wrap the await in try and catch the type with on:

dart
import 'dart:io';

Future<void> main() async {
  try {
    var text = await File('missing.txt').readAsString();
    print(text);
  } on FileSystemException catch (e) {
    print('Could not read the file.');
    print(e.message);
    print(e.path);
    print(e.osError?.message);
  }
  print('Program continues');
}
output
Could not read the file.
Cannot open file
missing.txt
No such file or directory
Program continues

message says what Dart tried, path names the file, and osError holds the operating system's reason. osError may be null, hence ?. (Lecture 014). Catching FileSystemException also catches PathNotFoundException, because it is a kind of FileSystemException.


11. Writing Piece by Piece with openWrite

For many small writes, openWrite() gives an IOSink — an open file you keep writing to. It has the same write and writeln as stdout:

dart
import 'dart:io';

Future<void> main() async {
  var file = File('report.txt');
  var sink = file.openWrite();
  sink.writeln('Report for Sara');
  for (var test = 1; test <= 3; test++) {
    sink.writeln('Test $test: ${70 + test * 5}');
  }
  await sink.close();

  stdout.write(await file.readAsString());
}
output
Report for Sara
Test 1: 75
Test 2: 80
Test 3: 85

writeln does not wait; the sink collects the text. await sink.close() writes everything out and closes the file — always close a sink. openWrite(mode: FileMode.append) adds to an existing file.


12. Reading a Large File Line by Line

readAsString loads the whole file into memory. For a huge log, read it as a stream (Lecture 092) instead. openRead() delivers raw bytes; utf8.decoder (from dart:convert) turns bytes into text, and LineSplitter cuts the text into lines:

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

Future<void> main() async {
  var file = File('log.txt');
  var sink = file.openWrite();
  for (var i = 1; i <= 100000; i++) {
    sink.writeln('Entry $i');
  }
  await sink.close();
  print('Size: ${await file.length()} bytes');

  var lines = file
      .openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter());

  var count = 0;
  await for (var line in lines) {
    count++;
    if (count <= 2) print(line);
  }
  print('Lines: $count');
}
output
Size: 1188895 bytes
Entry 1
Entry 2
Lines: 100000

Only a small piece of the file is in memory at any moment. transform is the same method from Lecture 095. We will study dart:convert and text encodings with JSON in Lecture 108.


13. A Real-World Example

Ali keeps his marks in a file, one subject,mark per line. Sara has no file yet:

dart
import 'dart:io';

Future<void> addMark(File file, String subject, int mark) async {
  await file.writeAsString('$subject,$mark\n', mode: FileMode.append);
}

Future<void> main() async {
  var file = File('ali_marks.txt');
  if (await file.exists()) {
    await file.delete();
  }

  await addMark(file, 'Math', 80);
  await addMark(file, 'Science', 75);
  await addMark(file, 'English', 85);

  var total = 0;
  for (var line in await file.readAsLines()) {
    var parts = line.split(',');
    var mark = int.parse(parts[1]);
    print('${parts[0]}: $mark');
    total += mark;
  }
  print('Total: $total');
  print('Average: ${total / 3}');

  try {
    await File('sara_marks.txt').readAsString();
  } on FileSystemException catch (e) {
    print('Sara: ${e.message} (${e.path})');
  }
}
output
Math: 80
Science: 75
English: 85
Total: 240
Average: 80.0
Sara: Cannot open file (sara_marks.txt)

exists and delete start a clean file, append adds each mark, readAsLines reads them back, and on FileSystemException handles the missing file without a crash.


Code Examples

Example 1 — Write and Read a Note

dart
import 'dart:io';

Future<void> main() async {
  var file = File('note.txt');
  await file.writeAsString('Buy milk');
  var note = await file.readAsString();
  print('Note: $note');
}
output
Note: Buy milk

Example 2 — Overwrite vs Append

dart
import 'dart:io';

Future<void> main() async {
  var file = File('visits.txt');
  await file.writeAsString('Ali\n');
  await file.writeAsString('Sara\n');
  print(await file.readAsLines());

  await file.writeAsString('Zayan\n', mode: FileMode.append);
  print(await file.readAsLines());
}
output
[Sara]
[Sara, Zayan]

Example 3 — A Sync Counter

dart
import 'dart:io';

void main() {
  var file = File('counter.txt');
  file.writeAsStringSync('1');
  var count = int.parse(file.readAsStringSync());
  file.writeAsStringSync('${count + 1}');
  print('Count: ${file.readAsStringSync()}');
}
output
Count: 2

Example 4 — Numbered Lines

dart
import 'dart:io';

Future<void> main() async {
  var file = File('fruits.txt');
  await file.writeAsString(['apple', 'mango', 'banana'].join('\n'));
  var fruits = await file.readAsLines();
  for (var i = 0; i < fruits.length; i++) {
    print('${i + 1}. ${fruits[i]}');
  }
}
output
1. apple
2. mango
3. banana

Example 5 — Delete Only If It Exists

dart
import 'dart:io';

Future<void> main() async {
  var file = File('draft.txt');
  await file.writeAsString('draft');
  if (await file.exists()) {
    await file.delete();
    print('Deleted ${file.path}');
  }
  print('Still there? ${await file.exists()}');
}
output
Deleted draft.txt
Still there? false

Example 6 — A Fallback for a Missing File

dart
import 'dart:io';

Future<String> loadName() async {
  try {
    return await File('name.txt').readAsString();
  } on FileSystemException {
    return 'Guest';
  }
}

Future<void> main() async {
  print('Welcome, ${await loadName()}');
  await File('name.txt').writeAsString('Sara');
  print('Welcome, ${await loadName()}');
}
output
Welcome, Guest
Welcome, Sara

on FileSystemException without catch (e) handles the error when we don't need its details.

Example 7 — Student Marks

dart
import 'dart:io';

Future<void> main() async {
  var file = File('sara_marks.txt');
  await file.writeAsString('88\n92\n79\n');

  var marks = (await file.readAsLines()).map(int.parse).toList();
  var total = marks.reduce((a, b) => a + b);
  print('Marks: $marks');
  print('Total: $total');
  print('Average: ${(total / marks.length).toStringAsFixed(1)}');
}
output
Marks: [88, 92, 79]
Total: 259
Average: 86.3

How it works

  1. readAsLines gives ['88', '92', '79'] — strings, not numbers.
  2. map(int.parse) converts each line, and toList() makes a List<int>.

Example 8 — Shopping Receipt with openWrite

dart
import 'dart:io';

Future<void> main() async {
  var cart = {'Notebook': 500, 'Pen': 50, 'Bag': 1500};
  var file = File('receipt.txt');

  var sink = file.openWrite();
  var total = 0;
  for (var item in cart.entries) {
    sink.writeln('${item.key}: ${item.value}');
    total += item.value;
  }
  sink.writeln('Total: $total');
  await sink.close();

  stdout.write(await file.readAsString());
}
output
Notebook: 500
Pen: 50
Bag: 1500
Total: 2050

Example 9 — Bank Transaction Log

dart
import 'dart:io';

Future<void> main() async {
  var log = File('bank_log.txt');
  await log.writeAsString('');

  Future<void> record(int amount) =>
      log.writeAsString('$amount\n', mode: FileMode.append);

  await record(1000);
  await record(-250);
  await record(400);

  var entries = await log.readAsLines();
  var balance = 0;
  for (var line in entries) {
    balance += int.parse(line);
  }
  print('Entries: ${entries.length}');
  print('Balance: $balance');
}
output
Entries: 3
Balance: 1150

How it works

  1. Writing '' empties the log, so every run starts the same.
  2. Each record appends one amount; withdrawals are negative.
  3. The balance is rebuilt from the log — the file is the record of truth.

Example 10 — Hottest Temperature, Streamed

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

Future<void> main() async {
  var file = File('temps.txt');
  await file.writeAsString('31.5\n29.0\n35.2\n33.8\n');

  var hottest = -100.0;
  var lines = file
      .openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter());
  await for (var line in lines) {
    var t = double.parse(line);
    if (t > hottest) hottest = t;
  }
  print('Hottest: $hottest');
}
output
Hottest: 35.2

Common Beginner Mistakes

1. Forgetting await

dart
import 'dart:io';

Future<void> main() async {
  var file = File('notes.txt');
  await file.writeAsString('Hello');
  print(file.readAsString()); // ✗ no await
}
output
Instance of 'Future<String>'

Write print(await file.readAsString());.

2. Using await in a plain main

dart
import 'dart:io';

void main() {
  var file = File('notes.txt');
  file.writeAsStringSync('Hi');
  // var text = await file.readAsString(); // ✗
}
output
Error: 'await' can only be used in 'async' or 'async*' methods.

Either make main Future<void> main() async, or use readAsStringSync().

3. Writing a number instead of a string

dart
import 'dart:io';

Future<void> main() async {
  var file = File('score.txt');
  // await file.writeAsString(80); // ✗
}
output
Error: The argument type 'int' can't be assigned to the parameter type 'String'.

Files hold text here: write '80' or '$score', and int.parse it when reading.

4. Overwriting a log by accident

Calling writeAsString without mode: FileMode.append in a loop leaves only the last line. There is no error — the data is just gone (Step 4).

5. Writing after close()

dart
import 'dart:io';

Future<void> main() async {
  var sink = File('report.txt').openWrite();
  sink.writeln('Report for Sara');
  await sink.close();
  // sink.writeln('Test 1: 75'); // ✗
}
output
Unhandled exception:
Bad state: StreamSink is closed

Do all the writing first, then close() once at the end.

6. Reading a file without handling a missing one

A file name in your code is no promise the file is there. Without try/catch, a missing file crashes the program with PathNotFoundException (Step 9).


Best Practices

  • Prefer the async methods with await; keep Sync versions for small scripts.
  • Use FileMode.append for logs and history; plain writes for settings and snapshots.
  • End each appended line with \n, so readAsLines sees separate lines.
  • Wrap reads in try with on FileSystemException, and give the user a fallback.
  • Always await sink.close() after openWrite.
  • Read large files with openRead and LineSplitter, not readAsString.

Mini Challenge

Zayan keeps a diary. Start diary.txt empty, append the entries Played cricket, Read a book and Helped Mum in a loop, then print how many entries there are and the last one.

Your program should print exactly:

output
Entries: 3
Last: Helped Mum

Try it yourself before opening the solution.

Show solution
dart
import 'dart:io';

Future<void> main() async {
  var diary = File('diary.txt');
  await diary.writeAsString('');
  for (var entry in ['Played cricket', 'Read a book', 'Helped Mum']) {
    await diary.writeAsString('$entry\n', mode: FileMode.append);
  }
  var lines = await diary.readAsLines();
  print('Entries: ${lines.length}');
  print('Last: ${lines.last}');
}
output
Entries: 3
Last: Helped Mum

How it works

  1. Writing '' empties the diary, so a second run doesn't double the entries.
  2. Each entry is appended with its own \n.
  3. readAsLines returns three lines; last is the newest entry.

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Write Hello, Sara! to greeting.txt, read it back with readAsString, and print it.
  2. Using only the Sync methods, write Zayan to name.txt and print the file's content.
  3. Write Ali to full_name.txt, then append Khan (no line break) and print Ali Khan.

Application

  1. Save the marks [80, 75, 90] to marks.txt, one per line. Read them with readAsLines and print Total: 245.
  2. Write Opening: 1000 to bank.txt, then append Deposit: 500 and Withdraw: 200, each on its own line. Print the whole file.
  3. For old.txt, print whether it exists, create it, print again, delete it, and print again. Expected: false, true, false.
  4. Read config.txt, which does not exist. Catch the error and print No config, using defaults.

Challenge

  1. With openWrite, write one line per Celsius value in [0, 10, 20, 30], like 0 C = 32.0 F. Close the sink, then read the lines back and print them.
  2. Write Notebook,500, Pen,50 and Bag,1500 to cart.txt. Read it with openRead, utf8.decoder and LineSplitter, and print Total: 2050.
  3. Write Future<String> readOrDefault(String path, String fallback) that returns the file's text, or fallback when the file cannot be read. Test it on an existing motto.txt (Keep learning) and a missing nothing.txt.

Practice Answers

Question 1 — Solution

dart
import 'dart:io';

Future<void> main() async {
  var file = File('greeting.txt');
  await file.writeAsString('Hello, Sara!');
  print(await file.readAsString());
}
output
Hello, Sara!

Question 2 — Solution

dart
import 'dart:io';

void main() {
  var file = File('name.txt');
  file.writeAsStringSync('Zayan');
  print(file.readAsStringSync());
}
output
Zayan

Question 3 — Solution

dart
import 'dart:io';

Future<void> main() async {
  var file = File('full_name.txt');
  await file.writeAsString('Ali');
  await file.writeAsString(' Khan', mode: FileMode.append);
  print(await file.readAsString());
}
output
Ali Khan

Question 4 — Solution

dart
import 'dart:io';

Future<void> main() async {
  var file = File('marks.txt');
  var marks = [80, 75, 90];
  await file.writeAsString(marks.join('\n'));

  var total = 0;
  for (var line in await file.readAsLines()) {
    total += int.parse(line);
  }
  print('Total: $total');
}
output
Total: 245

Question 5 — Solution

dart
import 'dart:io';

Future<void> main() async {
  var file = File('bank.txt');
  await file.writeAsString('Opening: 1000\n');
  await file.writeAsString('Deposit: 500\n', mode: FileMode.append);
  await file.writeAsString('Withdraw: 200\n', mode: FileMode.append);
  stdout.write(await file.readAsString());
}
output
Opening: 1000
Deposit: 500
Withdraw: 200

Question 6 — Solution

dart
import 'dart:io';

Future<void> main() async {
  var file = File('old.txt');
  print(await file.exists());
  await file.writeAsString('old data');
  print(await file.exists());
  await file.delete();
  print(await file.exists());
}
output
false
true
false

Question 7 — Solution

dart
import 'dart:io';

Future<void> main() async {
  try {
    print(await File('config.txt').readAsString());
  } on FileSystemException {
    print('No config, using defaults');
  }
}
output
No config, using defaults

Question 8 — Solution

dart
import 'dart:io';

Future<void> main() async {
  var file = File('temperatures.txt');
  var sink = file.openWrite();
  for (var c in [0, 10, 20, 30]) {
    sink.writeln('$c C = ${c * 9 / 5 + 32} F');
  }
  await sink.close();

  for (var line in await file.readAsLines()) {
    print(line);
  }
}
output
0 C = 32.0 F
10 C = 50.0 F
20 C = 68.0 F
30 C = 86.0 F

Question 9 — Solution

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

Future<void> main() async {
  var file = File('cart.txt');
  await file.writeAsString('Notebook,500\nPen,50\nBag,1500\n');

  var total = 0;
  var lines = file
      .openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter());
  await for (var line in lines) {
    total += int.parse(line.split(',')[1]);
  }
  print('Total: $total');
}
output
Total: 2050

Question 10 — Solution

dart
import 'dart:io';

Future<String> readOrDefault(String path, String fallback) async {
  try {
    return await File(path).readAsString();
  } on FileSystemException {
    return fallback;
  }
}

Future<void> main() async {
  await File('motto.txt').writeAsString('Keep learning');
  print(await readOrDefault('motto.txt', 'No motto'));
  print(await readOrDefault('nothing.txt', 'No motto'));
}
output
Keep learning
No motto

The return await matters: without await, the failing future would leave the try before its error arrives, and the catch would never see it.


Quick Revision

  1. Does File('notes.txt') create a file on the disk?
  2. What does writeAsString do to text already in the file?
  3. How do you add to the end of a file instead?
  4. What is the difference between readAsString and readAsStringSync?
  5. What does readAsLines return?
  6. Which exception type covers a missing file?
  7. What must you always do after writing with openWrite?
  8. Why read a huge file with openRead instead of readAsString?

Key Takeaways

  • File('name.txt') from dart:io points to a file; a relative name lives in the folder you run from.
  • writeAsString creates or replaces a file; readAsString returns its text — both are futures, so await them.
  • mode: FileMode.append adds to the end; add \n yourself.
  • Sync versions return values directly but block the program.
  • join('\n') saves a list; readAsLines reads it back as List<String>.
  • exists() and delete() check for and remove a file.
  • A missing file throws PathNotFoundException, a FileSystemException — catch it with on.
  • openWrite() gives an IOSink for many writes — always await sink.close().
  • openRead() + utf8.decoder + LineSplitter streams a large file line by line.

What's Next?

Lecture 107 — Directories & Paths. Every file in this lecture landed in the folder we ran from. Real programs keep files in folders of their own. Next we will create and list directories, and build file paths that work on every operating system.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.