Dart FileLock
last modified October 18, 2026
FileLock is a Dart class with constant values that specify the
locking mode when acquiring a lock on an open file via
RandomAccessFile.lock().
It is part of the dart:io library and coordinates file access
between competing processes or system-level components.
File locks in Dart are advisory on POSIX systems (Linux, macOS) and are
process-scoped record locks. On Windows, LockFileEx is used.
On POSIX, closing any descriptor for the file releases the process's
locks, so always release the lock and close the file in a
finally block.
FileLock Enum Values
The FileLock enum has four values that control whether the lock
is shared or exclusive and whether the acquisition blocks or fails immediately.
| Value | Blocking | Behaviour |
|---|---|---|
| FileLock.shared | No | Shared (read) lock. Multiple shared locks may coexist. Fails immediately if another process holds a conflicting exclusive lock. |
| FileLock.exclusive | No | Exclusive (write) lock. Fails immediately if another process holds a conflicting lock. |
| FileLock.blockingShared | Yes | Shared lock. The returned Future suspends until the lock can be acquired. |
| FileLock.blockingExclusive | Yes | Exclusive lock. The returned Future suspends until the lock can be acquired. |
Basic Exclusive Lock
This example shows the fundamental pattern: open a file, acquire an exclusive
lock via raf.lock(), write, then release with
raf.unlock().
import 'dart:io';
void main() async {
final file = File('data.txt');
final raf = await file.open(mode: FileMode.write);
try {
await raf.lock(FileLock.exclusive); // acquire lock
await raf.writeString('Protected content\n');
print('File written under exclusive lock');
} finally {
await raf.unlock(); // always release
await raf.close();
}
}
Locking is a separate step from opening: File.open() has no
lock parameter. Calling raf.unlock() in a
finally block ensures the lock is released even if the write
throws.
$ dart main.dart File written under exclusive lock
Shared Lock for Concurrent Reads
This example demonstrates using a shared lock so multiple readers can access the same file at the same time.
import 'dart:io';
import 'dart:convert';
Future<String> readShared(File file) async {
final raf = await file.open(mode: FileMode.read);
try {
await raf.lock(FileLock.shared);
final length = await raf.length();
final bytes = await raf.read(length);
return utf8.decode(bytes);
} finally {
await raf.unlock();
await raf.close();
}
}
void main() async {
final file = File('data.txt');
await file.writeAsString('shared content');
// Two concurrent shared reads succeed simultaneously
final results = await Future.wait([
readShared(file),
readShared(file),
]);
for (final content in results) {
print(content.trim());
}
}
Multiple shared locks coexist freely. Reading the file content through a
RandomAccessFile requires raf.read(length) followed
by utf8.decode() — there is no readAsString() on
RandomAccessFile.
$ dart main.dart shared content shared content
Blocking Exclusive Lock
This example shows how FileLock.blockingExclusive waits for a lock
instead of throwing, and how to serialise writers inside one isolate.
import 'dart:io';
// Serialises same-isolate callers; POSIX locks are per-process.
Future<void> _queue = Future.value();
Future<void> writeWithBlockingLock(
File file, String label, String content) {
final task = _queue.then((_) async {
final raf = await file.open(mode: FileMode.append);
try {
await raf.lock(FileLock.blockingExclusive);
print('$label: lock acquired');
await raf.writeString('$content\n');
print('$label: write done');
} finally {
await raf.unlock();
await raf.close();
}
});
_queue = task.then((_) {}, onError: (_) {});
return task;
}
void main() async {
final file = File('log.txt');
await file.writeAsString(''); // start with empty file
// Queue two writers inside this isolate
await Future.wait([
writeWithBlockingLock(file, 'Writer A', 'line from A'),
writeWithBlockingLock(file, 'Writer B', 'line from B'),
]);
print('Final content:');
print(await file.readAsString());
}
Future.wait starts both calls concurrently. On POSIX, locks are
associated with the process, so two descriptors opened by the same process do not
block each other; the module-level _queue serialises the writers
inside this isolate. FileLock.blockingExclusive still waits when
another process holds the lock.
$ dart main.dart Writer A: lock acquired Writer A: write done Writer B: lock acquired Writer B: write done Final content: line from A line from B
Lock Conflicts
This example demonstrates what happens when a non-blocking
FileLock.exclusive cannot be acquired because another
process holds the lock. A helper Dart process is started for the
demonstration, because POSIX locks do not conflict between descriptors of the
same process.
import 'dart:convert';
import 'dart:io';
/// Starts a separate Dart process that holds an exclusive lock.
Future<Process> startLockHolder(File file, Duration hold) async {
final helper = File('lock_holder.dart');
await helper.writeAsString('''
import 'dart:io';
Future<void> main(List<String> args) async {
final file = File(args[0]);
final raf = await file.open(mode: FileMode.append);
await raf.lock(FileLock.exclusive);
stdout.writeln('locked');
await Future.delayed(Duration(milliseconds: int.parse(args[1])));
await raf.unlock();
await raf.close();
}
''');
final process = await Process.start(
Platform.resolvedExecutable,
[helper.path, file.path, '${hold.inMilliseconds}'],
);
await process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.firstWhere((line) => line == 'locked');
return process;
}
void main() async {
final file = File('data.txt');
await file.writeAsString('content');
// The lock is held by another process, so contention is real
final holder = await startLockHolder(file, const Duration(seconds: 2));
print('First lock acquired');
// FileLock.exclusive is non-blocking: throws if the lock is held
final raf2 = await file.open(mode: FileMode.append);
try {
await raf2.lock(FileLock.exclusive);
print('Second lock acquired'); // not reached
} on FileSystemException catch (e) {
print('Lock conflict: ${e.message}');
} finally {
await raf2.close();
}
await holder.exitCode;
print('First lock released');
}
FileLock.exclusive throws a FileSystemException
immediately when the lock cannot be granted. Use
FileLock.blockingExclusive to wait instead of failing.
$ dart main.dart First lock acquired Lock conflict: lock failed First lock released
File Lock with Timeout
This example implements a try-lock loop that retries the non-blocking
FileLock.exclusive with a short back-off until a deadline is
reached.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
/// Starts a separate Dart process that holds an exclusive lock.
Future<Process> startLockHolder(File file, Duration hold) async {
final helper = File('lock_holder.dart');
await helper.writeAsString('''
import 'dart:io';
Future<void> main(List<String> args) async {
final file = File(args[0]);
final raf = await file.open(mode: FileMode.append);
await raf.lock(FileLock.exclusive);
stdout.writeln('locked');
await Future.delayed(Duration(milliseconds: int.parse(args[1])));
await raf.unlock();
await raf.close();
}
''');
final process = await Process.start(
Platform.resolvedExecutable,
[helper.path, file.path, '${hold.inMilliseconds}'],
);
await process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.firstWhere((line) => line == 'locked');
return process;
}
/// Tries to acquire an exclusive lock within [timeout].
/// Returns the locked [RandomAccessFile], or null on timeout.
Future<RandomAccessFile?> tryLockWithTimeout(
File file, Duration timeout) async {
final deadline = DateTime.now().add(timeout);
while (DateTime.now().isBefore(deadline)) {
final raf = await file.open(mode: FileMode.append);
try {
await raf.lock(FileLock.exclusive);
return raf; // acquired
} on FileSystemException {
await raf.close(); // busy, back off
await Future.delayed(const Duration(milliseconds: 100));
}
}
return null;
}
void main() async {
final file = File('data.txt');
await file.writeAsString('hello');
// A separate process holds the lock longer than the timeout
final holder = await startLockHolder(file, const Duration(seconds: 3));
print('Holder acquired lock');
final raf = await tryLockWithTimeout(file, const Duration(seconds: 1));
if (raf != null) {
print('Lock acquired within timeout');
await raf.unlock();
await raf.close();
} else {
print('Timed out waiting for lock');
}
await holder.exitCode;
print('Holder released lock');
}
The 100 ms back-off avoids busy-waiting. The holder is a separate process, so
the lock actually conflicts; it releases the lock after 3 seconds, after the
1-second deadline has expired, and tryLockWithTimeout returns
null.
$ dart main.dart Holder acquired lock Timed out waiting for lock Holder released lock
Byte-Range Locking
raf.lock() accepts optional start and end
byte parameters so you can lock only a specific region of a file. This lets
concurrent operations work on non-overlapping sections simultaneously.
import 'dart:io';
import 'dart:typed_data';
const recordSize = 16; // bytes per fixed-size record
Future<void> updateRecord(File file, int index, Uint8List data) async {
final start = index * recordSize;
final end = start + recordSize;
final raf = await file.open(mode: FileMode.append);
try {
await raf.lock(FileLock.exclusive, start, end); // range-lock
print('Record $index locked (bytes $start-${end - 1})');
await raf.setPosition(start);
await raf.writeFrom(data);
print('Record $index updated');
} finally {
await raf.unlock(start, end); // release same range
await raf.close();
}
}
void main() async {
final file = File('records.bin');
// Initialise 3 blank records
await file.writeAsBytes(Uint8List(recordSize * 3));
// Records 0 and 2 are non-overlapping, so both locks succeed concurrently
await Future.wait([
updateRecord(file, 0, Uint8List.fromList(List.filled(recordSize, 0xAA))),
updateRecord(file, 2, Uint8List.fromList(List.filled(recordSize, 0xBB))),
]);
final bytes = await file.readAsBytes();
print('Record 0, byte 0: 0x${bytes[0].toRadixString(16).padLeft(2, '0')}');
print('Record 2, byte 32: 0x${bytes[32].toRadixString(16).padLeft(2, '0')}');
}
The same [start, end) range must be passed to both
lock() and unlock(). Locks on non-overlapping
ranges are independent, so records 0 and 2 can be updated at the same time.
$ dart main.dart Record 0 locked (bytes 0-15) Record 2 locked (bytes 32-47) Record 0 updated Record 2 updated Record 0, byte 0: 0xaa Record 2, byte 32: 0xbb
Safe Read-Modify-Write
A common concurrency hazard is a read-modify-write cycle where two processes read the same old value before either has written back. An exclusive lock over the entire cycle prevents this.
import 'dart:io';
import 'dart:convert';
Future<int> incrementCounter(File file) async {
final raf = await file.open(mode: FileMode.append);
try {
await raf.lock(FileLock.blockingExclusive);
// Read current value
await raf.setPosition(0);
final length = await raf.length();
final raw = utf8.decode(await raf.read(length)).trim();
final count = int.tryParse(raw) ?? 0;
// Write incremented value
final next = count + 1;
await raf.truncate(0);
await raf.setPosition(0);
await raf.writeString('$next');
return next;
} finally {
await raf.unlock();
await raf.close();
}
}
void main() async {
final file = File('counter.txt');
if (!await file.exists()) await file.writeAsString('0');
for (var i = 0; i < 3; i++) {
final value = await incrementCounter(file);
print('Counter is now: $value');
}
}
FileMode.append opens the file for reading and writing without
truncating it. raf.truncate(0) clears the file before writing
the new value so no stale bytes remain. The full read-modify-write cycle is
atomic because blockingExclusive keeps other callers waiting.
$ dart main.dart Counter is now: 1 Counter is now: 2 Counter is now: 3
Lock Guard Helper
Repeating the open/lock/unlock/close boilerplate at every call site is error-prone. A generic helper wraps it and guarantees the lock is always released.
import 'dart:io';
// Serialises same-isolate callers; POSIX fcntl locks are per-process and
// therefore do not block multiple descriptors opened by the same process.
Future<void> _queue = Future.value();
/// Opens [file], acquires a blocking exclusive lock, runs [action],
/// then unlocks and closes — even if [action] throws.
///
/// Calls within the same isolate are serialised via [_queue]; the OS lock
/// guards concurrent access from other processes.
Future<T> withExclusiveLock<T>(
File file,
Future<T> Function(RandomAccessFile raf) action) {
final task = _queue.then((_) async {
final raf = await file.open(mode: FileMode.append);
try {
await raf.lock(FileLock.blockingExclusive);
return await action(raf);
} finally {
await raf.unlock();
await raf.close();
}
});
// Advance the queue; swallow errors so one failure doesn't stall later calls.
_queue = task.then((_) {}, onError: (_) {});
return task;
}
void main() async {
final logFile = File('app.log');
await logFile.writeAsString(''); // clear log
Future<void> appendLog(String message) =>
withExclusiveLock(logFile, (raf) async {
final pos = await raf.length();
await raf.setPosition(pos);
await raf.writeString(
'${DateTime.now().toIso8601String()} $message\n');
});
// Concurrent log writes are serialised by the blocking lock
await Future.wait([
appendLog('Server started'),
appendLog('Connection accepted'),
appendLog('Request processed'),
]);
print(await logFile.readAsString());
}
The higher-order function withExclusiveLock eliminates the
possibility of forgetting unlock() — the finally block runs even
if the action throws. Each appendLog call is one line, and all
lock bookkeeping is hidden in the helper.
There is a subtlety on Linux: Dart's FileLock.blockingExclusive
uses POSIX fcntl record locks, which are per-process.
Multiple file descriptors opened by the same process therefore never
block each other, so the OS lock alone cannot serialise concurrent calls
within one isolate. The module-level _queue future chain fills
that gap: each call appends itself to the chain and only starts after the
previous one completes. The OS lock still matters — it prevents races with
other processes that open the same file.
$ dart main.dart 2026-05-29T10:00:00.000 Server started 2026-05-29T10:00:00.001 Connection accepted 2026-05-29T10:00:00.002 Request processed
Best Practices
- Call lock() after open():
File.open()has nolockparameter; always acquire the lock as a separate step withraf.lock(). - Always unlock in finally: Place
raf.unlock()andraf.close()in afinallyblock so they run even when an exception is thrown. - Non-blocking vs blocking: Use
shared/exclusive(non-blocking) when you want to fail fast and handle contention yourself. UseblockingShared/blockingExclusivewhen contention is rare and waiting is acceptable. - Lock the full operation: For read-modify-write cycles, keep the lock held for the entire sequence — not just the write — to prevent lost updates.
- Byte-range granularity: For structured binary files, lock only the record or section being modified so other parts of the file remain accessible.
- Consistent lock order: When acquiring locks on multiple files, always acquire them in the same order across all code paths to avoid deadlocks.
- Use a helper: A generic
withExclusiveLockwrapper makes call sites concise and prevents unlock omissions.
Source
This tutorial covered Dart's FileLock enum with practical examples:
acquiring exclusive and shared locks, using blocking locks to serialise
concurrent writers, detecting and handling lock conflicts, implementing a
timeout loop, locking specific byte ranges, performing safe read-modify-write
cycles, and building a reusable lock-guard helper.
Author
List all Dart tutorials.