In Dart's dart:io, what is the difference between File.readAsString() and File.readAsStringSync(), and when is the Sync variant acceptable?
answer
- methods come in pairs
- Future versus the value itself
- one event loop per isolate
- short scripts versus a UI isolate
- PathNotFoundException either way
basics
~20 sreadAsString() returns a Future<String> so the isolate keeps handling events while the file is read; readAsStringSync() returns the String directly and blocks the isolate until the read finishes. Sync suits short scripts, not a Flutter UI isolate.
solid answer
~40 sMost `File` and `Directory` operations in `dart:io` come in pairs: `readAsString`/`readAsStringSync`, `exists`/`existsSync`, `list`/`listSync`. The async one returns a `Future` (or a `Stream` for `list`), and while the read is outstanding the isolate's event loop keeps running timers, input and other callbacks. The `Sync` one returns the value directly, but the calling isolate executes nothing else until the operating system call returns. The library's own docs say to prefer the async version unless you have a specific reason. Sync is reasonable in a short command-line script that has nothing else to do; on a Flutter app's main isolate a slow disk read becomes a stalled frame. Both variants fail with a `FileSystemException` subtype such as `PathNotFoundException` - the async one through its Future, the sync one by throwing.
code
dart · 14 linesimport 'dart:io';
Future<String?> loadConfig(String path) async {
try {
return await File(path).readAsString();
} on PathNotFoundException {
return null;
}
}
void main() async {
final config = await loadConfig('tool.yaml');
print(config ?? 'no config, using defaults');
}go deeper
Recall that the Sync suffix means the call returns the value directly and blocks, while the plain name returns a Future you await.
Explain the single event loop per isolate and why a blocking call stops timers, input and frames, plus how errors arrive in each variant.
Show judgment about where sync I/O is harmless (startup scripts) versus harmful (servers, UI isolates), and avoid check-then-act races on files.
Frame the tradeoff as simplicity versus responsiveness and describe a team rule for which code paths may use Sync file APIs.
## Two versions of almost every file operation The `dart:io` library gives Dart programs on native platforms (the Dart VM, compiled executables and non-web Flutter apps) access to the file system. Its `File`, `Directory` and `Link` classes expose most operations as **pairs**: an **asynchronous** method and a **synchronous twin** whose name ends in `Sync`. The class documentation for `File` says it directly: unless you have a specific reason for the synchronous version, prefer the asynchronous one so you do not block your program. | Async method | Returns | Sync twin | Returns | |---|---|---|---| | `readAsString()` | `Future<String>` | `readAsStringSync()` | `String` | | `readAsBytes()` | `Future<Uint8List>` | `readAsBytesSync()` | `Uint8List` | | `writeAsString(s)` | `Future<File>` | `writeAsStringSync(s)` | `void` | | `exists()` | `Future<bool>` | `existsSync()` | `bool` | | `Directory.list()` | `Stream<FileSystemEntity>` | `Directory.listSync()` | `List<FileSystemEntity>` | ## What blocking means in Dart Dart code runs in **isolates**, and each isolate has exactly one **event loop** that runs one piece of Dart code at a time. There is no second Dart thread in that isolate waiting to take over. - With `await file.readAsString()`, the Dart code hands the request to the runtime's I/O machinery and returns to the event loop. Timers, incoming socket data, user input and, in Flutter, frame callbacks keep being processed. When the data is ready, the Future completes and the code after `await` resumes. - With `file.readAsStringSync()`, the isolate waits inside that call until the operating system has delivered the whole file. Nothing else in that isolate runs - no timer, no callback, no frame. On a fast local SSD and a small file the difference may be invisible. On a network drive, a large file, a slow SD card or a busy device, the sync call can take tens or hundreds of milliseconds. In a Flutter app the main isolate is also the one that builds and lays out frames, so a slow `readAsStringSync()` there is a dropped frame the user can see. ## When the Sync twin is a reasonable choice 1. **A short command-line script** that reads a config file at startup and then does its work. Nothing else is waiting on the event loop, so blocking costs nothing and the code reads more simply. 2. **Code that genuinely cannot be async**, such as a synchronous callback required by an API you do not control. 3. **Tests or build scripts** where simplicity matters more than responsiveness. It is a poor choice in a server handling concurrent requests (one slow read stalls every client on that isolate) and on a Flutter UI isolate. ## Errors look the same, delivered differently Both variants report failures with subclasses of `FileSystemException`: `PathNotFoundException` for a missing path, `PathAccessException` for a permission problem, `PathExistsException` when something is already there. The async method delivers the error through its Future, so you catch it with `try`/`catch` around the `await`. The sync method throws at the call site. ## Common mistakes - **Check-then-read races.** Calling `existsSync()` and then reading is not atomic: the file can be deleted in between. Read and catch `PathNotFoundException` instead. - **Assuming async means parallel Dart code.** The Dart code still runs on one event loop; only the waiting is overlapped. - **Hiding a sync call behind an `async` function.** Marking a function `async` does not make `readAsStringSync()` inside it non-blocking. - **Reading huge files whole** with either variant - for large inputs, `openRead()` streams chunks instead.
- Why is calling existsSync() before reading a file a weak pattern?The check and the read are two separate operations, so another process can delete or replace the file in between. You still have to handle the failure on read. Reading directly and catching `PathNotFoundException` is simpler and covers the race.
- Does wrapping readAsStringSync() in an async function stop it from blocking?No. `async` only changes how the function returns its result; the body still runs on the same isolate's event loop. The sync call blocks that isolate for as long as the read takes. Use the async method, or move the work to another isolate if it must stay synchronous.
saying these in an interview costs you the question
- Says readAsStringSync runs on a background thread automatically
- Thinks marking a function async makes sync file calls inside it non-blocking
- Believes async file reads run Dart code in parallel on the same isolate
- Checks existsSync before every read instead of handling PathNotFoundException
- Uses sync file I/O on a Flutter UI isolate without considering frame time