ZetCode

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.

ValueBlockingBehaviour
FileLock.sharedNoShared (read) lock. Multiple shared locks may coexist. Fails immediately if an exclusive lock is held.
FileLock.exclusiveNoExclusive (write) lock. Fails immediately if any lock is held by another open file description.
FileLock.blockingSharedYesShared lock. The returned Future suspends until the lock can be acquired.
FileLock.blockingExclusiveYesExclusive 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().

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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

Source

Dart FileLock Documentation

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.