ZetCode

Dart File System Watchers

last modified May 29, 2026

Introduction

A file system watcher lets a program monitor a directory (or directories) for changes: file creation, modification, deletion, and moves. Dart provides this capability through the Directory.watch() method, which returns a Stream<FileSystemEvent>. The watcher is non-blocking and works by asking the operating system to notify us when something happens inside the watched directory.

Watchers are essential for any tool that must react to filesystem changes in real time. Common examples include auto-reloading development servers that restart when source code changes, log monitors that tail growing files, and build tools that recompile assets on save. Without a watcher, such programs would have to poll the filesystem repeatedly, wasting CPU and battery.

detect_support.dart
import 'dart:io';

void main() {
  // Watchers rely on OS-specific native APIs. The Dart runtime will
  // throw a FileSystemException if watching is not supported (rare).
  final dir = Directory('.');

  try {
    dir.watch(); // just a syntax check - returns a Stream
    print('Directory watching is available on this platform.');
  } on FileSystemException catch (e) {
    print('Watching is not supported: $e');
  }
}

The Directory.watch() method is available on all major desktop platforms (Linux, macOS, Windows). It uses the most efficient native API under the hood and provides a consistent Dart-side interface.

How Directory.watch() works

Calling watch() on a Directory object returns a lazy Stream<FileSystemEvent>. You listen to this stream with .listen() or an await for loop. Each emitted event is one of four concrete types:

Event typeTriggerExample
FileSystemCreateEventFile or directory createdtouch new.txt
FileSystemModifyEventFile content or metadata changedEditor saves a file
FileSystemDeleteEventFile or directory deletedrm old.txt
FileSystemMoveEventFile/directory renamed or moved (also includes the destination)mv a.txt b.txt

Every event carries a path property (an absolute path to the affected file or directory). FileSystemMoveEvent additionally holds destination (the new path). The stream never closes on its own — it emits events until you cancel the subscription or the program exits.

basic_watch.dart
import 'dart:io';

void main() {
  final dir = Directory('watched_folder');
  if (!dir.existsSync()) dir.createSync();

  print('Watching ${dir.path} …');
  final stream = dir.watch(recursive: false);

  final subscription = stream.listen((FileSystemEvent event) {
    print('${event.runtimeType} → ${event.path}');
  });

  // Keep the script alive (press Enter to quit)
  stdin.listen((_) => subscription.cancel());
}

The recursive parameter (default false) controls whether subdirectories are also monitored. When true, the stream yields events for any file system object inside the entire tree.

Filtering events

Raw event streams can be noisy — a single save often triggers several modify events. Use Dart's stream methods to filter and transform the stream before acting on it.

filtered_watch.dart
import 'dart:io';

void main() {
  final dir = Directory('watched_folder')
    ..createSync(recursive: true);

  dir
      .watch(recursive: true)
      .where((event) => !event.path.endsWith('.swp')) // ignore vim temp files
      .where((event) => event is FileSystemModifyEvent)
      .map((event) => 'Modified: ${event.path}')
      .distinct()
      .listen(print);

  print('Watching. Press Enter to exit.');
  stdin.listen((_) => exit(0));
}

Here where drops unwanted files, map converts the event into a readable string, and distinct suppresses consecutive duplicate lines. These operators keep the handling logic clean and let you focus on what actually matters.

Real-world use cases

Auto-reloading HTTP server

A typical Dart development server can watch its own source directory and restart itself whenever a .dart file changes. A simple implementation listens for modify events, kills the current isolate, and spawns a new one — a poor man's hot-reload.

Log file monitor

If an application writes to a log file, a watcher on the log directory can detect when the file is rotated (delete + create) or appended to (modify), and a separate process can tail the new content in real time.

Build tool trigger

A lightweight build script may watch lib/ for any change and automatically re-run the Dart compiler or a test suite. Combined with a debouncer, it ensures compilation only happens once after a burst of saves.

simple_autoreloader.dart
import 'dart:io';
import 'dart:async';

void main() {
  final dir = Directory('lib');
  if (!dir.existsSync()) {
    print('No lib/ directory found.');
    return;
  }

  Timer? debounce;

  dir.watch(recursive: true).listen((event) {
    if (event is FileSystemModifyEvent && event.path.endsWith('.dart')) {
      // Debounce: wait 300ms of silence before acting.
      debounce?.cancel();
      debounce = Timer(Duration(milliseconds: 300), () {
        print('Source changed. Running dart analyze …');
        Process.run('dart', ['analyze', '.']).then((result) {
          stdout.write(result.stdout);
          stderr.write(result.stderr);
        });
      });
    }
  });

  print('Auto-reloader active. Watching lib/ …');
}

The debounce timer prevents multiple rapid rebuilds when several files are saved in quick succession (e.g., a “Save All” in an IDE). This pattern is the cornerstone of most Dart-based build watchers.

Platform differences

Under the hood, Directory.watch() uses the best available OS API. The Dart runtime abstracts away most platform details, but a few behavioural differences remain:

PlatformAPI usedNotes
LinuxinotifyVery efficient. Supports recursive watches natively. Watches an inode, so renames of watched directories can lose the watch.
macOSFSEventsExcellent performance. Reports coarse-grained events (a whole directory tree can be reported with one event). The Dart stream still yields individual events per path.
WindowsReadDirectoryChangesWWorks well for local NTFS drives. Network drives and some virtual filesystems may not send events reliably. Buffer overflows can occur under extreme load (Dart handles this gracefully).

Symlinks are generally followed on Linux and macOS when using recursive: true. On Windows, symlink support depends on the filesystem and Dart version. Network mounts and fuse-based filesystems may not generate events at all or may produce duplicate events. Always test on the target deployment platform.

Another subtle difference: FileSystemModifyEvent may fire more than once for a single logical change, especially on macOS where the save lifecycle often involves a temporary file and an atomic rename. The Dart stream does not coalesce these automatically — your code should be prepared to handle multiple modify events per “save”.

platform_check.dart
import 'dart:io';

void main() {
  print('Running on: ${Platform.operatingSystem}');
  final dir = Directory('.');

  // Some paths may not be watchable.
  try {
    dir.watch();
    print('Watching is supported on current directory.');
  } catch (e) {
    print('Watch not supported: $e');
  }
}

Always wrap the initial watch() call in a try-catch block when the path may point to a network location or a special filesystem, to avoid crashing the process.

Common pitfalls

deduplication_example.dart
import 'dart:io';
import 'dart:async';

void main() {
  final dir = Directory('watched')..createSync();

  // Simple deduplication: ignore events for the same path
  // that occur within 200ms of each other.
  final lastSeen = <String, DateTime>{};

  dir.watch(recursive: true).listen((event) {
    final now = DateTime.now();
    final prev = lastSeen[event.path];

    if (prev != null && now.difference(prev) < Duration(milliseconds: 200)) {
      return; // skip duplicate
    }
    lastSeen[event.path] = now;

    print('[${now.hour}:${now.minute}:${now.second}.${now.millisecond}] '
          '${event.runtimeType} ${event.path}');
  });

  print('Watching. Press Enter to stop.');
  stdin.listen((_) => exit(0));
}

The map-based deduplication above is a lightweight alternative to full debouncing. It works well when you want immediate feedback for the first event but wish to suppress subsequent noise for the same path.

Complete runnable example — recursive watcher with timestamps

The program below watches a directory tree recursively, debounces rapid events, and prints every change together with a high-precision timestamp. It handles both local paths and command-line arguments, making it a useful standalone diagnostic tool.

file_watcher.dart
import 'dart:io';
import 'dart:async';

/// Prints the time, event kind, and path with ANSI colours for clarity.
void logEvent(String kind, String path) {
  final now = DateTime.now();
  final ts = '${now.hour.toString().padLeft(2,'0')}:'
             '${now.minute.toString().padLeft(2,'0')}:'
             '${now.second.toString().padLeft(2,'0')}.'
             '${now.millisecond.toString().padLeft(3,'0')}';

  // Simple colour mapping (works on most terminals).
  final colours = {
    'CREATE': '\x1B[32m',  // green
    'MODIFY': '\x1B[33m',  // yellow
    'DELETE': '\x1B[31m',  // red
    'MOVE'  : '\x1B[36m',  // cyan
  };
  final reset = '\x1B[0m';
  final colour = colours[kind] ?? '';

  stdout.writeln('$ts  $colour$kind$reset  $path');
}

Future<void> main(List<String> args) async {
  final target = args.isNotEmpty ? args.first : 'watched_dir';
  final dir = Directory(target);

  if (!dir.existsSync()) {
    stderr.writeln('Directory "$target" does not exist.');
    exit(1);
  }

  print('Watching "${dir.path}" recursively …\n');

  // Map of active timers for debouncing (one per path).
  final debounceTimers = <String, Timer>{};

  final stream = dir.watch(recursive: true);

  final subscription = stream.listen(
    (FileSystemEvent event) {
      // Cancel any pending timer for this path.
      debounceTimers[event.path]?.cancel();

      // Set a new timer: only report the event after 250ms of quiet.
      debounceTimers[event.path] = Timer(
        const Duration(milliseconds: 250),
        () {
          String kind;
          String path = event.path;

          if (event is FileSystemCreateEvent) {
            kind = 'CREATE';
          } else if (event is FileSystemModifyEvent) {
            kind = 'MODIFY';
          } else if (event is FileSystemDeleteEvent) {
            kind = 'DELETE';
          } else if (event is FileSystemMoveEvent) {
            kind = 'MOVE';
            path = '${event.path} → ${event.destination}';
          } else {
            kind = 'UNKNOWN';
          }

          logEvent(kind, path);
        },
      );
    },
    onError: (error) {
      stderr.writeln('Watch error: $error');
    },
  );

  // Keep running until the user presses Enter.
  stdin.listen((_) {
    print('\nShutting down watcher…');
    subscription.cancel();
    for (final timer in debounceTimers.values) {
      timer.cancel();
    }
    exit(0);
  });
}

The script creates a debounce timer per file path. When a new event arrives, any pending timer for that path is cancelled and a fresh 250 ms timer is started. This means a burst of three modify events in 100 ms produces only one log line. The programme distinguishes all four event types and uses ANSI escape codes to highlight them in different colours (green for create, yellow for modify, red for delete, cyan for move).

To test the watcher, run the script and create, edit, rename, and delete files inside the watched directory. You will see clean, colour-coded output with precise timestamps. Press Enter to stop the watcher gracefully.

Source

Directory.watch() documentation, Dart server & CLI tutorial, watcher package on pub.dev

In this tutorial we covered Dart's file system watcher: how Directory.watch() returns a stream of FileSystemEvent objects, how to filter and debounce events, platform-specific behaviour, and a fully worked example that logs changes with timestamps.

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.