ZetCode

Dart Error Handling and Exceptions

last modified May 29, 2026

Introduction to Exceptions vs. Errors

Dart distinguishes between two kinds of problem conditions: Exceptions are intended to be caught and handled, while Errors indicate serious programmatic mistakes that should not normally be caught. Both can be thrown and caught, but the intention differs.

An Exception (or its subclass) signals a predictable problem that your code can recover from — a malformed string, a network timeout, or a missing key in a map. An Error (like ArgumentError or AssertionError) signals a bug: a function was called with an invalid argument, or an internal invariant was violated. The convention is to catch exceptions for graceful recovery and to let errors propagate and crash, because they indicate a defect in the program itself.

CategoryPurposeExamplesTypical action
ExceptionRecoverable runtime conditionsFormatException, TimeoutException, FileSystemExceptionCatch and handle gracefully
ErrorProgramming errors, logic bugsAssertionError, ArgumentError, RangeErrorDo not catch; fix the code

The table above summarises the conceptual split. It is a convention rather than a strict rule — nothing prevents you from catching an Error, but doing so is generally discouraged because it masks bugs that should be fixed.

exception_vs_error.dart
void main() {
  // An Exception: you can catch and handle it.
  try {
    int.parse('not-a-number');
  } on FormatException catch (e) {
    print('Handled format problem: $e');
  }

  // An Error: usually not caught, because it indicates a bug.
  // The next line would throw ArgumentError if uncommented.
  // List<int>.filled(-5, 0); // throws ArgumentError (negative length)
}

Understanding this distinction helps you decide what to catch and when to let the program fail fast.

The try, catch, and finally Blocks

Wrap any code that might throw in a try block. Following it, catch blocks execute if an exception occurs. The finally block always runs — whether an exception was thrown or not — and is ideal for cleanup tasks like closing files or streams.

basic_try_catch.dart
void main() {
  try {
    final result = 100 ~/ 0; // integer division by zero
    print('Result: $result');
  } catch (e) {
    print('Something went wrong: $e');
  } finally {
    print('Cleanup: this always runs.');
  }

  // Program continues normally.
  print('Program ended.');
}

The catch clause here is a catch‑all that catches everything. While convenient, it is usually better to catch specific exception types (see the next section). The finally block executes even if return is called inside try or catch, making it reliable for resource release.

Catching Specific Exceptions using on

Dart’s on keyword lets you catch only a particular exception type. Multiple on/catch blocks can be chained, each handling a different exception. This is the recommended practice because it avoids swallowing unexpected errors.

specific_catch.dart
void main() {
  const inputs = ['42', '0', 'not-a-number'];

  for (final str in inputs) {
    try {
      final number = int.parse(str);
      final division = 100 ~/ number;
      print('100 / $number = $division');
    } on FormatException {
      print('"$str" is not a valid integer.');
    } on IntegerDivisionByZeroException {
      print('Cannot divide by zero.');
    } catch (e) {
      // Fallback for any other unexpected exception
      print('Unexpected error: $e');
    }
  }
}

Here each on block names a specific exception class and captures the exception object in the variable e (optional). The final catch acts as a safety net for anything not explicitly named. Without it, an unhandled exception would crash the program.

Inspecting the Stack Trace

When an exception is thrown, Dart captures a stack trace that shows the sequence of function calls leading to the error. To access it, use the two‑parameter catch syntax: catch (e, s). The second parameter receives the StackTrace object.

stack_trace.dart
void parseOrThrow(String input) {
  final n = int.parse(input);
  final result = 100 ~/ n;
  print('Result: $result');
}

void main() {
  try {
    parseOrThrow('not-a-number');
  } on FormatException catch (e, s) {
    print('Caught FormatException: $e');
    print('Stack trace:\n$s');
  }
}

The stack trace is invaluable during development and for logging in production. It pinpoints exactly where the problem originated, even if the exception was thrown several calls deep. In release builds, consider logging the trace rather than printing it directly to the console.

Throwing Exceptions and the rethrow Keyword

You can throw any object in Dart, but it is best practice to throw instances of Exception or Error subclasses. Use the throw keyword followed by an expression.

throw_example.dart
class NegativeAgeException implements Exception {
  final int age;
  NegativeAgeException(this.age);

  @override
  String toString() => 'Age cannot be negative: $age';
}

void verifyAge(int age) {
  if (age < 0) {
    throw NegativeAgeException(age);
  }
  print('Age $age is valid.');
}

void main() {
  try {
    verifyAge(-5);
  } on NegativeAgeException catch (e) {
    print('Validation failed: $e');
  }
}

The rethrow keyword is used inside a catch clause to pass the exception up the call stack while still performing some local action (like logging). It preserves the original stack trace, unlike a plain throw which would reset it.

rethrow_example.dart
void processFile(String path) {
  try {
    // simulate a file read error
    throw FileSystemException('File not found', path);
  } catch (e, s) {
    print('Local log: problem with $path → $e');
    rethrow; // propagates the original exception and stack trace
  }
}

void main() {
  try {
    processFile('/tmp/missing.txt');
  } catch (e, s) {
    print('Global handler: $e');
    print('Original stack trace preserved:\n$s');
  }
}

Use rethrow when a mid‑level function needs to note the exception but cannot fully resolve it. The caller then gets the original exception with its full context intact.

Creating Custom Exceptions

Define your own exception classes by implementing the Exception interface and overriding toString(). This gives you expressive, domain‑specific exceptions that callers can catch specifically.

custom_exception.dart
class InsufficientFundsException implements Exception {
  final double requested;
  final double available;

  InsufficientFundsException(this.requested, this.available);

  @override
  String toString() =>
      'Insufficient funds: requested $requested, '
      'available $available (shortfall ${requested - available})';
}

class BankAccount {
  final String owner;
  double balance;

  BankAccount(this.owner, this.balance);

  void withdraw(double amount) {
    if (amount > balance) {
      throw InsufficientFundsException(amount, balance);
    }
    balance -= amount;
    print('Withdrawn $amount. New balance: $balance');
  }
}

void main() {
  final account = BankAccount('Alice', 100.0);

  try {
    account.withdraw(30.0);
    account.withdraw(200.0); // will throw
  } on InsufficientFundsException catch (e) {
    print('Transaction declined: $e');
  }
}

Custom exceptions carry meaningful data and provide a clear message when printed. They make error handling specific and readable. Because they implement Exception, they fit naturally into Dart’s exception hierarchy and can be caught with on clauses.

Complete runnable example — resilient user input processor

The following programme demonstrates all the concepts together. It reads simulated user input, parses integers, divides them, and handles various failure modes with specific exception types, logging, and finally cleanup.

error_handling_demo.dart
import 'dart:io';

class InvalidOperationException implements Exception {
  final String operation;
  InvalidOperationException(this.operation);

  @override
  String toString() => 'Invalid operation: $operation';
}

void executeOperation(String op, int a, int b) {
  switch (op) {
    case '+':
      print('$a + $b = ${a + b}');
      break;
    case '-':
      print('$a - $b = ${a - b}');
      break;
    case '*':
      print('$a * $b = ${a * b}');
      break;
    case '/':
      print('$a / $b = ${a ~/ b}'); // may throw
      break;
    default:
      throw InvalidOperationException(op);
  }
}

void main() {
  const inputs = ['12/4', '10/0', '17+3', 'abc', '100^2'];

  for (final input in inputs) {
    try {
      // Parse simple "a op b" (e.g., "12/4")
      final parts = input.split(RegExp(r'([+\-*/])'));
      if (parts.length != 2) throw FormatException('Bad format: $input');

      final a = int.parse(parts[0]);
      final b = int.parse(parts[1]);
      final op = input[input.indexOf(RegExp(r'[+\-*/]'))];

      executeOperation(op, a, b);
    } on FormatException catch (e, s) {
      stderr.writeln('Input error: $e');
      // optional: log s to file
    } on IntegerDivisionByZeroException {
      stderr.writeln('Math error: division by zero in "$input".');
    } on InvalidOperationException catch (e) {
      stderr.writeln('Unsupported operation: $e');
    } catch (e, s) {
      stderr.writeln('Unexpected error: $e');
      stderr.writeln('Stack:\n$s');
    } finally {
      stdout.writeln('---'); // separator for readability
    }
  }
}

The loop processes each input string. Specific exceptions are caught with on, the stack trace is available when needed, and the finally block prints a separator after every iteration, whether or not an error occurred. The program never crashes — all issues are handled gracefully.

Source

Dart error handling documentation, Exception class API, Error class API

In this tutorial we covered Dart’s error handling mechanics: the distinction between exceptions and errors, the try/catch/ finally structure, catching specific exception types with on, extracting stack traces, throwing custom exceptions, and using rethrow to preserve trace information.

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.