Dart FileLock
last modified May 29, 2026
FileLock is a Dart enum that specifies 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
implemented using flock(2). On Windows, LockFileEx
is used. Locks are tied to an open file description: calling
raf.unlock() or closing the RandomAccessFile
always releases the lock.
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 an exclusive lock is held. |
| FileLock.exclusive | No | Exclusive (write) lock. Fails immediately if any lock is held by another open file description. |
| 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 serialises
concurrent writers without throwing exceptions.
import 'dart:io';
Future<void> writeWithBlockingLock(
File file, String label, String content) async {
final raf = await file.open(mode: FileMode.append);
try {
// Suspends until any existing lock is released
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();
}
}
void main() async {
final file = File('log.txt');
await file.writeAsString(''); // start with empty file
// Launch two concurrent writers; blocking lock serialises them
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 coroutines concurrently, but
blockingExclusive queues the second writer until the first
finishes, so no data is lost and no exception is thrown.
$ 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 open file
description already holds the lock.
import 'dart:io';
void main() async {
final file = File('data.txt');
await file.writeAsString('content');
final raf1 = await file.open(mode: FileMode.append);
await raf1.lock(FileLock.exclusive);
print('First lock acquired');
// FileLock.exclusive is non-blocking: throws if lock is already 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(); // close without unlock
}
await raf1.unlock();
await raf1.close();
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: Failed to lock file 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:io';
import 'dart:async';
/// 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');
// Simulate a process holding the lock for the full timeout period
final holder = await file.open(mode: FileMode.append);
await holder.lock(FileLock.exclusive);
print('Holder acquired lock');
final raf = await tryLockWithTimeout(file, const Duration(seconds: 2));
if (raf != null) {
print('Lock acquired within timeout');
await raf.unlock();
await raf.close();
} else {
print('Timed out waiting for lock');
}
await holder.unlock();
await holder.close();
print('Holder released lock');
}
The 100 ms back-off avoids busy-waiting. Because the holder never releases
while tryLockWithTimeout is running, the loop exhausts the
deadline and 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.