ZetCode

Dart Process

last modified May 29, 2026

The Process class in Dart provides functionality to run and interact with system processes. It is part of Dart's dart:io library and is available in command-line and server-side applications.

Process allows executing external commands, reading their output, writing to their stdin, monitoring exit codes, and terminating processes with signals. It is essential for system-level programming, build tooling, and shell automation in Dart.

Basic Definition

Process represents a native OS process started by the Dart VM. The two primary entry points are Process.start and Process.run. Choose based on your use case:

FeatureProcess.startProcess.run
ReturnsFuture<Process>Future<ProcessResult>
Output handlingStreaming — chunks arrive as dataBuffered — all output collected in memory
Blocks until exitNo — returns immediatelyYes — awaits process completion
stdin accessFull writable streamNot accessible after start
Memory useConstant — streams are not bufferedProportional to total output size
Best forLong-running, interactive, large outputShort commands, simple scripts

Both methods are asynchronous and return a Future. Use ProcessSignal to send signals such as SIGTERM or SIGKILL to a running process.

Running a Simple Command

This example starts an external command with Process.start and streams its output.

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  var process = await Process.start('ls', ['-l']);

  await process.stdout.transform(utf8.decoder).forEach(print);

  var exitCode = await process.exitCode;
  print('Exit code: $exitCode');
}

Process.start launches the command and returns a Process object immediately. The stdout property is a byte stream; we decode it with utf8.decoder and print each chunk. Awaiting process.exitCode blocks until the process terminates and yields its numeric exit code (0 means success).

$ dart main.dart
total 4
-rw-r--r-- 1 user user 241 May 29 10:00 main.dart
Exit code: 0

Handling Input and Output

This example demonstrates two-way communication with a process through its stdin and stdout streams.

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  var process = await Process.start('grep', ['--color=never', 'dart']);

  // Write lines to the process stdin
  process.stdin.writeln('This line contains dart');
  process.stdin.writeln('This line does not match');
  process.stdin.writeln('Another dart reference here');
  await process.stdin.close(); // signal EOF so grep can finish

  // Collect matching lines from stdout
  var output = await process.stdout.transform(utf8.decoder).join();
  print(output.trim());

  await process.exitCode;
}

We pipe text into grep's stdin and collect only the lines that match the pattern from stdout. Calling stdin.close() sends an EOF signal, which tells the child process that no more input is coming. Without it, grep would wait forever.

$ dart main.dart
This line contains dart
Another dart reference here

Capturing stdout and stderr Simultaneously

When a process writes to both stdout and stderr, you must listen to both streams concurrently. Subscribing to one stream and then the other risks a deadlock: if the process fills the unread pipe buffer it will stall, and your code never reaches the second listen call.

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  var process = await Process.start(
    'sh', ['-c', 'echo "normal output"; echo "error output" 1>&2; exit 1'],
  );

  // Subscribe to both streams before awaiting either
  process.stdout
      .transform(utf8.decoder)
      .listen((data) => print('OUT: ${data.trim()}'));

  process.stderr
      .transform(utf8.decoder)
      .listen((data) => print('ERR: ${data.trim()}'));

  final code = await process.exitCode;
  print('Exit: $code');
}

Both listen calls register non-blocking callbacks that fire whenever data arrives. Only after both are subscribed do we await exitCode. The event loop services both pipes in parallel so neither buffer fills up.

OUT: normal output
ERR: error output
Exit: 1

Avoiding Deadlocks

Deadlocks occur when your code waits for the process to finish while the process is blocked waiting for a reader to consume its output. Common pitfalls:

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  var process = await Process.start(
    'sh', ['-c', 'for i in $(seq 1 1000); do echo "line $i"; done'],
  );

  // Start both consumers before awaiting either result
  final stdoutFuture = process.stdout.transform(utf8.decoder).join();
  final stderrFuture = process.stderr.transform(utf8.decoder).join();

  // Wait for both streams, then check exit code
  final results = await Future.wait([stdoutFuture, stderrFuture]);
  final exitCode = await process.exitCode;

  print('Lines : ${results[0].trim().split('\n').length}');
  print('Errors: "${results[1]}"');
  print('Exit  : $exitCode');
}

join() returns a Future<String> that resolves when the stream closes. Using Future.wait lets the event loop drain both pipes concurrently, so neither buffer stalls the child. Only after both futures resolve — meaning both streams are fully consumed — do we await the exit code.

Lines : 1000
Errors: ""
Exit  : 0

Piping Processes Together

Dart has no built-in shell pipe operator, but you can connect two processes by forwarding the stdout of the first into the stdin of the second using Stream.pipe.

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  // Equivalent of: ls -1 /usr/bin | grep 'zip'
  var p1 = await Process.start('ls', ['-1', '/usr/bin']);
  var p2 = await Process.start('grep', ['zip']);

  // Wire p1 stdout → p2 stdin; pipe() closes p2.stdin when p1.stdout ends
  p1.stdout.pipe(p2.stdin);

  // Drain p1 stderr to prevent it from blocking
  p1.stderr.drain<void>();

  final output = await p2.stdout.transform(utf8.decoder).join();
  await Future.wait([p1.exitCode, p2.exitCode]);

  print(output.trim());
}

Stream.pipe forwards every chunk from the source stream to the given StreamSink and automatically closes the sink when the source ends. This sends an EOF to grep's stdin, letting it flush its output and exit cleanly. Always drain the stderr of any process whose stdout you are piping to avoid a secondary deadlock.

bunzip2
bzip2
gzip
unzip
zip
zipgrep

Collecting Output with Process.run

Process.run is a convenience wrapper around Process.start that waits for the process to exit and buffers all output. It is ideal for short commands where you only care about the final result.

main.dart
import 'dart:io';

void main() async {
  var result = await Process.run('date', ['+%Y-%m-%d %H:%M:%S']);

  if (result.exitCode == 0) {
    print('Date: ${result.stdout.trim()}');
  } else {
    print('Error: ${result.stderr}');
    exitCode = result.exitCode;
  }
}

Process.run returns a ProcessResult containing stdout, stderr, and exitCode. Both output properties are strings when stdoutEncoding is not explicitly set to null. Checking exitCode is good practice before trusting the output.

$ dart main.dart
Date: 2026-05-29 10:32:11

ProcessResult and Large Output

Process.run accumulates the entire output in memory before returning. This is convenient for small commands but becomes a problem when output is large — for example find / or a long-running build log. Use Process.start and process the stream line by line when the output size is unbounded.

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  // Process.run buffers everything — risky for large output
  // var result = await Process.run('find', ['/']);

  // Process.start streams output — safe for any size
  var process = await Process.start('find', ['/usr', '-name', '*.dart']);
  process.stderr.drain<void>(); // discard stderr without blocking

  var count = 0;
  await process.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .forEach((_) => count++);

  await process.exitCode;
  print('Found $count .dart files');
}

LineSplitter splits the decoded text into individual lines so each line can be acted on without buffering the full output. Memory use stays constant regardless of how many lines the command produces. Use Process.run for commands that produce at most a few MB; switch to Process.start for anything larger.

$ dart main.dart
Found 1842 .dart files

Handling Errors

Two distinct failure modes exist: the process cannot be launched at all (throws ProcessException), and the process runs but exits with a non-zero code (check exitCode and stderr).

main.dart
import 'dart:io';

void main() async {
  // Case 1: executable not found
  try {
    await Process.run('nonexistent_cmd', []);
  } on ProcessException catch (e) {
    print('Could not start process: ${e.message} (errno ${e.errorCode})');
  }

  // Case 2: process starts but fails
  var result = await Process.run('ls', ['/no/such/directory']);
  if (result.exitCode != 0) {
    print('Command failed (exit ${result.exitCode}): ${result.stderr.trim()}');
  }
}

ProcessException is thrown when the OS cannot locate or start the executable. It carries a human-readable message and a numeric errorCode. A non-zero exitCode is the conventional way programs signal failure after a successful start; always read stderr in that case for the diagnostic message.

$ dart main.dart
Could not start process: No such file or directory (errno 2)
Command failed (exit 2): ls: cannot access '/no/such/directory': No such file or directory

Understanding StdioType

When Dart spawns a child process, it creates OS-level pipes for stdout and stderr so that your Dart code can read from them as streams. The child process therefore does not see a terminal (TTY) on those descriptors — it sees the write end of a pipe. This is why programs that detect whether their output goes to a terminal (for example, to decide whether to enable ANSI colours or buffering) will behave differently when started from Dart.

The StdioType enum from dart:io describes what a file descriptor actually is: StdioType.terminal, StdioType.pipe, or StdioType.other. The top-level stdioType() function returns the type for a given stream.

main.dart
import 'dart:io';

void main() async {
  // Report what the parent process sees
  print('Parent stdin  : ${stdioType(stdin)}');
  print('Parent stdout : ${stdioType(stdout)}');
  print('Parent stderr : ${stdioType(stderr)}');

  // Spawn a child that reports its own stdio types
  var result = await Process.run('dart', ['child.dart']);
  print('\nChild reports:\n${result.stdout.trim()}');
}
child.dart
import 'dart:io';

void main() {
  print('stdin  : ${stdioType(stdin)}');
  print('stdout : ${stdioType(stdout)}');
  print('stderr : ${stdioType(stderr)}');
}

Even when the parent's stdout is a terminal, the child's stdout and stderr are reported as StdioType.pipe because Dart wired OS pipes between parent and child. The child's stdin is reported as StdioType.other — it is also a pipe, but the read end rather than the write end.

Parent stdin  : StdioType.terminal
Parent stdout : StdioType.terminal
Parent stderr : StdioType.terminal

Child reports:
stdin  : StdioType.other
stdout : StdioType.pipe
stderr : StdioType.pipe

The practical consequence is that tools like ls --color, grep --color=auto, and many CLI programs will suppress colour output when called from Dart. Pass an explicit flag (e.g. --color=always) or strip ANSI codes from the output if you need plain text.

Running Multiple Processes Concurrently

This example launches several independent processes at the same time and collects their results with Future.wait.

main.dart
import 'dart:io';

void main() async {
  var futures = [
    Process.run('uname', ['-s']),
    Process.run('date', ['+%Y-%m-%d']),
    Process.run('hostname', []),
  ];

  var results = await Future.wait(futures);

  for (var result in results) {
    if (result.exitCode == 0) {
      print(result.stdout.trim());
    } else {
      print('Error: ${result.stderr.trim()}');
    }
  }
}

Future.wait takes a list of futures and resolves when every one of them completes. The calls are started concurrently, not sequentially, so the total wall-clock time is roughly that of the slowest command. The results list preserves the original order regardless of which process finishes first.

$ dart main.dart
Linux
2026-05-29
workstation

Setting Working Directory

The optional workingDirectory parameter controls which directory the child process sees as its current working directory. This is useful when a command must be run from a specific location, for example a build tool that expects its project files nearby.

main.dart
import 'dart:io';

void main() async {
  final tmpDir = Directory.systemTemp.path;

  // Create a temporary file so 'ls' has something to show
  final file = File('$tmpDir/dart_demo.txt');
  await file.writeAsString('hello from Dart');

  var result = await Process.run(
    'ls',
    ['-lh', 'dart_demo.txt'],
    workingDirectory: tmpDir,
  );

  print(result.stdout.trim());
  print('Exit code: ${result.exitCode}');

  await file.delete();
}

Without workingDirectory, relative paths in command arguments are resolved against the Dart script's working directory. Providing an explicit path makes behaviour predictable regardless of where the script is invoked from. The value must be an absolute path to an existing directory.

$ dart main.dart
-rw-r--r-- 1 user user 15 May 29 10:42 dart_demo.txt
Exit code: 0

Setting Environment Variables

The environment parameter passes a custom set of environment variables to the child process. By default the child inherits the parent's environment; supplying this map replaces it entirely unless you merge it with Platform.environment.

main.dart
import 'dart:io';

void main() async {
  final env = {
    ...Platform.environment, // inherit existing env
    'APP_ENV': 'production',
    'APP_VERSION': '2.1.0',
    'LOG_LEVEL': 'warn',
  };

  var result = await Process.run(
    'printenv',
    ['APP_ENV', 'APP_VERSION', 'LOG_LEVEL'],
    environment: env,
  );

  print(result.stdout.trim());
  print('Exit code: ${result.exitCode}');
}

Spreading Platform.environment first preserves PATH and other variables needed for the command to run correctly, then the custom entries override or extend them. This pattern is common for configuring deployment environments, injecting secrets, or varying runtime behaviour without touching config files.

$ dart main.dart
production
2.1.0
warn
Exit code: 0

Killing a Process

Long-running processes sometimes need to be aborted. The process.kill method sends a POSIX signal; the default is ProcessSignal.sigterm, which asks the process to shut down gracefully. Use ProcessSignal.sigkill if it does not respond.

main.dart
import 'dart:io';

void main() async {
  var process = await Process.start('sleep', ['60']);
  print('Started process PID: ${process.pid}');

  // Simulate a timeout: kill after 2 seconds
  final timer = Future.delayed(const Duration(seconds: 2), () {
    final sent = process.kill(ProcessSignal.sigterm);
    print('SIGTERM sent: $sent');
  });

  await timer;
  final code = await process.exitCode;
  print('Process exited with code: $code');
}

process.kill returns true if the signal was delivered and false if the process had already exited. On Linux/macOS, a process terminated by SIGTERM typically exits with code -15; one killed by SIGKILL exits with -9. Always await process.exitCode after killing to release the OS resources associated with the process.

$ dart main.dart
Started process PID: 18432
SIGTERM sent: true
Process exited with code: -15

Timeout Handling

A process that hangs can block your program indefinitely. Use the timeout extension on Future to set a deadline and kill the process if it is exceeded.

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

void main() async {
  var process = await Process.start('sleep', ['30']);
  print('PID: ${process.pid}');

  final code = await process.exitCode.timeout(
    const Duration(seconds: 3),
    onTimeout: () {
      print('Timed out — killing process');
      process.kill(ProcessSignal.sigkill);
      return -1; // synthetic exit code returned to the caller
    },
  );

  print('Exit code: $code');
}

Future.timeout fires onTimeout when the deadline expires without the future resolving. Using sigkill guarantees termination; sigterm only requests it and the process may ignore it. The onTimeout callback returns a synthetic exit code so the rest of the program can continue normally.

PID: 24871
Timed out — killing process
Exit code: -1

Launching GUI Applications

Process.start can open GUI applications just like any other executable. Because the launched app has its own window and event loop, there is normally no need to read its output or wait for it to exit. The example below detects the current platform and opens a ubiquitous text editor on each OS.

main.dart
import 'dart:io';

void main() async {
  if (Platform.isWindows) {
    // Notepad - built into every Windows installation
    await Process.start('notepad.exe', [], mode: ProcessStartMode.detached);
  } else if (Platform.isMacOS) {
    // TextEdit - the default plain-text editor on macOS
    await Process.start('open', ['-a', 'TextEdit'],
        mode: ProcessStartMode.detached);
  } else if (Platform.isLinux) {
    // Try common editors in order; fall back gracefully
    final editors = ['gedit', 'kate', 'mousepad', 'xed'];
    bool launched = false;
    for (final editor in editors) {
      try {
        await Process.start(editor, [],
            mode: ProcessStartMode.detached);
        print('Opened $editor');
        launched = true;
        break;
      } on ProcessException {
        print('Could not launch $editor, trying next...');
        continue; // editor not installed, try next
      }
    }
    if (!launched) print('No known text editor found.');
  }
}

ProcessStartMode.detached tells the OS to decouple the child from the Dart process: the editor stays open even after the Dart program exits, its stdout/stderr are not connected to the parent, and the Dart program does not need to await its exit code. On macOS, the open -a idiom delegates to the system launcher, which resolves the correct application bundle automatically.

Reading Binary Output

Some tools produce binary data — compressed archives, raw checksums, image bytes. Read the raw byte stream directly instead of passing it through a text decoder, which would corrupt the data.

main.dart
import 'dart:io';
import 'dart:typed_data';

void main() async {
  // Compress a string via gzip and capture the raw binary output
  var process = await Process.start('gzip', ['-c']);
  process.stdin.write('Hello, Dart!');
  await process.stdin.close();

  // Collect raw byte chunks — do not use utf8.decoder here
  final chunks = <List<int>>[];
  await for (final chunk in process.stdout) {
    chunks.add(chunk);
  }
  await process.exitCode;

  final bytes = Uint8List.fromList(chunks.expand((c) => c).toList());
  print('Compressed ${bytes.length} bytes');
  // gzip files always start with magic bytes 0x1f 0x8b
  print('Magic: 0x${bytes[0].toRadixString(16)}${bytes[1].toRadixString(16)}');
}

process.stdout is a Stream<List<int>> that yields raw byte buffers. Iterating it with await for collects every chunk without any text decoding. Flattening with expand and wrapping in Uint8List produces a contiguous byte array ready for file writes or further processing.

Compressed 28 bytes
Magic: 0x1f8b

Windows vs Linux/macOS Differences

Dart's Process API is cross-platform, but the underlying commands and shell conventions differ. The most important differences to keep in mind:

main.dart
import 'dart:io';

Future<ProcessResult> listFiles(String dir) {
  if (Platform.isWindows) {
    return Process.run('cmd', ['/c', 'dir', dir]);
  } else {
    return Process.run('ls', ['-l', dir]);
  }
}

void main() async {
  final result = await listFiles('.');
  if (result.exitCode == 0) {
    print(result.stdout);
  } else {
    print('Error: ${result.stderr}');
  }
}

Using Platform.isWindows / Platform.isLinux / Platform.isMacOS is the standard way to branch on the host OS. For maximum portability, isolate all platform-specific command strings behind helper functions like the one above.

Real-World Example: Running git

This example runs git log to retrieve the five most recent commit messages, processes the output line by line, and prints a structured summary. It combines working directory control, environment passthrough, and streaming output handling in a single realistic script.

main.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  final repoDir = Directory.current.path;

  final process = await Process.start(
    'git',
    ['log', '--oneline', '-5', '--no-color'],
    workingDirectory: repoDir,
    environment: {
      ...Platform.environment,
      'GIT_TERMINAL_PROMPT': '0', // disable interactive credential prompts
    },
  );

  // Drain stderr in the background so it never blocks the process
  process.stderr.drain<void>();

  final lines = await process.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .toList();

  final code = await process.exitCode;
  if (code != 0) {
    print('git failed (exit $code). Is this a git repository?');
    return;
  }

  print('Recent commits in: $repoDir');
  for (final line in lines) {
    final spaceIndex = line.indexOf(' ');
    final hash = line.substring(0, spaceIndex);
    final message = line.substring(spaceIndex + 1);
    print('  [$hash] $message');
  }
}

process.stderr.drain() discards stderr output while still consuming it, preventing a deadlock if git writes warnings. GIT_TERMINAL_PROMPT=0 prevents git from hanging the script waiting for credentials when run against a private remote.

Recent commits in: /home/user/myproject
  [a1b2c3d] Fix null safety issue in process handler
  [e4f5a6b] Add timeout support to process runner
  [c7d8e9f] Refactor stream handling
  [1a2b3c4] Update README
  [9f8e7d6] Initial commit

Best Practices

Source

Dart Process Documentation

This tutorial covered Dart's Process class with practical examples showing command execution, I/O handling, deadlock avoidance, process piping, timeout handling, binary output, cross-platform differences, and real-world usage.

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.