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.
| Category | Purpose | Examples | Typical action |
|---|---|---|---|
Exception | Recoverable runtime conditions | FormatException, TimeoutException, FileSystemException | Catch and handle gracefully |
Error | Programming errors, logic bugs | AssertionError, ArgumentError, RangeError | Do 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.
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.
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.
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.
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.
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.
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.
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.
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
List all Dart tutorials.