Dart: Dart Exception Handling
Last updated: 2026-08-26
Exception handling is the safety net of your code — without it, a single error can crash the entire system.
1. What You Will Learn
- Semantic difference between Exception and Error
- Complete syntax of try / on / catch / finally
- Custom exception classes and error code design
- rethrow and exception chaining
- Bob's Scenario: DataPipeline file parsing exception and network timeout handling
2. A Developer's Real Story
(1) The Pain Point: Unhandled Exceptions Crash Batch Processing Midway
Bob's DataPipeline, while processing millions of orders, crashed due to a malformed CSV line causing a FormatException. All 800,000 processed records were lost, requiring a complete re-run. Worse, the error message only showed "FormatException" with no line number or context, and it took Bob 4 hours to locate the problem.
(2) The Exception Handling Solution
Use try-on-catch-finally to catch specific exceptions, custom exception classes to carry context information, and finally to ensure resource cleanup.
try {
final records = await parseCsvFile(path);
await processRecords(records);
} on FormatException catch (e) {
log.error('CSV parse error: ${e.message} at line ${e.offset}');
// Skip malformed records, continue processing
} on TimeoutException {
log.error('API timeout, retrying...');
await retryWithBackoff();
} finally {
await closeResources();
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
(3) The Benefits
- A single record error no longer causes the entire batch to fail
- Custom exceptions carry context like line numbers and fields, reducing location time from 4 hours to 5 minutes
finallyensures file handles and network connections are always released
3. Exception vs Error
(1) Semantic Difference
flowchart TD A[Throwable] --> B[Error<br/>Recoverable: NO] A --> C[Exception<br/>Recoverable: YES] B --> B1[OutOfMemoryError] B --> B2[StackOverflowError] C --> C1[FormatException] C --> C2[TimeoutException] C --> C3[IOException] C --> C4[Custom Exception]
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
| Aspect | Error | Exception |
|---|---|---|
| Recoverability | Not recoverable | Recoverable |
| Should be caught | No | Yes |
| Produced by | VM / Runtime | Application code |
| Example | StackOverflowError | FormatException |
4. try / on / catch / finally
(1) Complete Syntax
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:Basic try-catch
void main() {
try {
final result = int.parse('abc');
print(result);
} on FormatException catch (e) {
print('Format error: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
}
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:Difference between on and catch
void main() {
// on - catches specific type, no access to exception object
try {
int.parse('not a number');
} on FormatException {
print('Caught FormatException (no details needed)');
}
// on + catch - catches specific type WITH access to exception
try {
int.parse('not a number');
} on FormatException catch (e) {
print('Caught: ${e.message}');
}
// catch with stack trace
try {
int.parse('not a number');
} on FormatException catch (e, stackTrace) {
print('Error: $e');
print('Stack: $stackTrace');
}
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:The finally block
import 'dart:io';
void main() async {
File? file;
try {
file = File('orders.csv');
final content = await file.readAsString();
print('Read ${content.length} characters');
} on FileSystemException catch (e) {
print('File error: ${e.message}');
} finally {
// Always executed - even if return or throw
print('Cleanup: file handle released');
}
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
| Clause | Purpose | Can have multiple | Order |
|---|---|---|---|
try |
Wraps code that may throw an exception | 1 | First |
on Type |
Catches a specific type | Multiple | After try |
catch (e) |
Catches any exception | 1 | After on |
finally |
Always executed | 1 | Last |
5. Custom Exception Classes
(1) Exception Class Design Principles
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:Custom exception classes
// Base exception for DataPipeline
class PipelineException implements Exception {
final String message;
final String? source;
final int? lineNumber;
PipelineException(this.message, {this.source, this.lineNumber});
@override
String toString() => 'PipelineException: $message'
'${source != null ? " (source: $source)" : ""}'
'${lineNumber != null ? " at line $lineNumber" : ""}';
}
// Specific exception types
class DataFormatException extends PipelineException {
final String fieldName;
final String invalidValue;
DataFormatException({
required this.fieldName,
required this.invalidValue,
required super.message,
super.source,
super.lineNumber,
});
@override
String toString() => 'DataFormatException: $message '
'(field: $fieldName, value: "$invalidValue")';
}
class NetworkTimeoutException extends PipelineException {
final Duration timeout;
final String endpoint;
NetworkTimeoutException({
required this.timeout,
required this.endpoint,
super.message = 'Request timed out',
}) : super(message);
@override
String toString() => 'NetworkTimeout: ${timeout.inSeconds}s '
'timeout on $endpoint';
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:Error code design
enum ErrorCode {
fileNotFound('E001', 'File not found'),
invalidFormat('E002', 'Invalid data format'),
networkTimeout('E003', 'Network request timed out'),
authFailed('E004', 'Authentication failed'),
rateLimitExceeded('E005', 'Rate limit exceeded');
final String code;
final String description;
const ErrorCode(this.code, this.description);
}
class CodedException extends PipelineException {
final ErrorCode errorCode;
CodedException(this.errorCode, {String? detail})
: super('${errorCode.code}: ${errorCode.description}'
'${detail != null ? " - $detail" : ""}');
@override
String toString() => '[$errorCode] $message';
}
void main() {
try {
throw CodedException(ErrorCode.invalidFormat, detail: 'amount field is not a number');
} on CodedException catch (e) {
print(e); // [ErrorCode.invalidFormat] E002: Invalid data format - amount field is not a number
}
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
6. rethrow and Exception Chaining
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:rethrow
double parseAmount(String input) {
try {
return double.parse(input);
} on FormatException catch (e) {
// Log and rethrow - don't swallow the exception
print('Failed to parse amount: "$input"');
rethrow; // Preserves original stack trace
}
}
void main() {
try {
final amount = parseAmount('not_a_number');
} on FormatException {
print('Caught rethrown exception');
}
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:Exception chaining
class ChainedException implements Exception {
final String message;
final Exception? innerException;
ChainedException(this.message, {this.innerException});
@override
String toString() {
var result = 'ChainedException: $message';
if (innerException != null) {
result += '\n Caused by: $innerException';
}
return result;
}
}
Future``<double>`` fetchOrderAmount(String orderId) async {
try {
// Simulate API call
throw FormatException('Invalid JSON response');
} on FormatException catch (e) {
throw ChainedException(
'Failed to fetch order $orderId',
innerException: e,
);
}
}
void main() async {
try {
await fetchOrderAmount('ORD-001');
} on ChainedException catch (e) {
print(e);
}
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
7. Bob's Scenario: DataPipeline Exception Handling
▶ Example
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
:Complete exception handling flow
import 'dart:async';
// Custom exceptions
class PipelineException implements Exception {
final String message;
PipelineException(this.message);
@override
String toString() => 'PipelineException: $message';
}
class CsvParseException extends PipelineException {
final int lineNumber;
CsvParseException(String message, this.lineNumber) : super(message);
@override
String toString() => 'CsvParseException: $message (line $lineNumber)';
}
// Safe CSV parser with exception handling
List<Map<String, String>> parseCsv(String content) {
final lines = content.split('\n');
if (lines.isEmpty) throw PipelineException('Empty CSV content');
final headers = lines[0].split(',');
final records = <Map<String, String>>[];
for (var i = 1; i < lines.length; i++) {
final line = lines[i].trim();
if (line.isEmpty) continue;
try {
final values = line.split(',');
if (values.length != headers.length) {
throw CsvParseException(
'Column count mismatch: expected ${headers.length}, got ${values.length}',
i + 1,
);
}
final record = <String, String>{};
for (var j = 0; j < headers.length; j++) {
record[headers[j].trim()] = values[j].trim();
}
records.add(record);
} on CsvParseException {
rethrow;
} catch (e) {
throw CsvParseException('Unexpected error: $e', i + 1);
}
}
return records;
}
Future``<void>`` processData(String csvContent) async {
List<Map<String, String>>? records;
try {
records = parseCsv(csvContent);
print('Parsed ${records.length} records');
// Simulate API call with timeout
await Future.delayed(const Duration(seconds: 1));
print('Data submitted successfully');
} on CsvParseException catch (e) {
print('Parse error: $e - skipping malformed records');
} on TimeoutException catch (e) {
print('Network timeout: $e - will retry later');
} on PipelineException catch (e) {
print('Pipeline error: $e');
} finally {
print('Cleanup: resources released');
}
}
void main() async {
final csv = 'id,amount,status\nORD-001,1500,completed\nORD-002,50,pending\nORD-003,bad_data,completed';
await processData(csv);
print('\n--- Test with malformed CSV ---');
final badCsv = 'id,amount\nORD-001,1500,completed'; // Wrong column count
await processData(badCsv);
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
8. Complete Example: DataPipeline Robust Data Processing
// ============================================
// DataPipeline Robust Data Processing
// Full exception handling with custom exceptions
// ============================================
import 'dart:async';
// Error codes
enum PipelineError {
fileNotFound('E001', 'File not found'),
invalidFormat('E002', 'Invalid data format'),
networkTimeout('E003', 'Network timeout'),
validationFailed('E004', 'Validation failed');
final String code;
final String label;
const PipelineError(this.code, this.label);
}
class PipelineException implements Exception {
final PipelineError error;
final String detail;
final Exception? cause;
PipelineException(this.error, {this.detail = '', this.cause});
@override
String toString() => '[${error.code}] ${error.label}'
'${detail.isNotEmpty ? ": $detail" : ""}'
'${cause != null ? " (caused by: $cause)" : ""}';
}
// Order with validation
class Order {
final String id;
final double amount;
final String status;
Order({required this.id, required this.amount, required this.status}) {
if (id.isEmpty) {
throw PipelineException(PipelineError.validationFailed, detail: 'Order ID is empty');
}
if (amount <= 0) {
throw PipelineException(PipelineError.validationFailed, detail: 'Amount must be positive: $amount');
}
}
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status)';
}
// Robust order parser
class OrderParser {
final List``<PipelineException>`` _errors = [];
int parsed = 0;
int skipped = 0;
List``<PipelineException>`` get errors => List.unmodifiable(_errors);
Order? tryParse(Map<String, dynamic> data) {
try {
final order = Order(
id: (data['id'] ?? '') as String,
amount: (data['amount'] as num).toDouble(),
status: (data['status'] ?? 'unknown') as String,
);
parsed++;
return order;
} on PipelineException catch (e) {
_errors.add(e);
skipped++;
return null;
} on TypeError catch (e) {
_errors.add(PipelineException(
PipelineError.invalidFormat,
detail: 'Type mismatch in record: $e',
));
skipped++;
return null;
}
}
void printReport() {
print('Parsed: $parsed, Skipped: $skipped');
if (_errors.isNotEmpty) {
print('Errors:');
for (final e in _errors) {
print(' $e');
}
}
}
}
void main() {
final rawData = <Map<String, dynamic>>[
{'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed'},
{'id': '', 'amount': 500.0, 'status': 'pending'}, // Invalid: empty ID
{'id': 'ORD-003', 'amount': -50.0, 'status': 'completed'}, // Invalid: negative amount
{'id': 'ORD-004', 'amount': 'not_a_number', 'status': 'pending'}, // Invalid: wrong type
{'id': 'ORD-005', 'amount': 3200.0, 'status': 'completed'},
];
final parser = OrderParser();
final validOrders = ``<Order>``[];
for (final data in rawData) {
final order = parser.tryParse(data);
if (order != null) validOrders.add(order);
}
print('=== DataPipeline Processing Report ===');
parser.printReport();
print('\nValid Orders:');
for (final order in validOrders) {
print(' $order');
}
final totalRevenue = validOrders.fold``<double>``(0, (s, o) => s + o.amount);
print('\nTotal Revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
}
> **Output:** Run in a local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly depending on the SDK version.
Output:
=== DataPipeline Processing Report ===
Parsed: 3, Skipped: 2
Errors:
[E004] Validation failed: Order ID is empty
[E004] Validation failed: Amount must be positive: -50.0
[E002] Invalid data format: Type mismatch in record: ...
Valid Orders:
Order(ORD-001, $1500.00, completed)
Order(ORD-005, $3200.00, completed)
Total Revenue: $4700.00 USD
❓ FAQ
Q: Does Dart have checked exceptions? A: No. All exceptions in Dart are unchecked; the compiler does not enforce declaration or catching. This provides flexibility but requires developers to handle exceptions consciously.
Q: What is the difference between
onandcatch? A:on Typecatches a specific exception type without binding a variable (unlesscatchis added).catch (e)catches any exception and binds it to a variable. Usually, the combinationon Type catch (e)is used.
Q: What is the use of
catchandrethrow? A:catchallows you to log, perform cleanup, etc., after catching an exception. Then userethrowto re-throw the same exception so upper layers can continue handling it.rethrowpreserves the original stack trace, which is better thanthrow e.
Q: When does the
finallyblock execute? A: Thefinallyblock always executes, regardless of whether an exception is thrown in thetryblock, whether it's caught, or whether there is areturnstatement. The only exception is if the program is killed (e.g., via SIGKILL).
Q: What should custom exceptions inherit from? A: It is recommended to
implement Exceptionrather thanextend Exception.implementsis more flexible, not being restricted by single inheritance. This is also the approach recommended by the official Dart guidelines.
Q: Does exception handling impact performance? A: The try-catch block itself has near-zero overhead (the Dart VM doesn't add extra instructions inside try blocks). However, the creation and throwing of exceptions have a cost and should not be used as a normal flow control mechanism.
Q: How can I avoid swallowing exceptions? A: An empty
catchblock is a code smell. At least log the exception, orrethrow. If you must ignore it, usecatch (_) {}and add a comment explaining why.
📖 Summary
- Exceptions are recoverable and should be caught; Errors are not recoverable and should not be caught
- Complete try-on-catch-finally syntax:
oncatches by type,catchbinds a variable,finallyalways executes - Custom exceptions
implement Exception, carrying context (line number, field, error code) rethrowpreserves the original stack trace; exception chaining helps trace the root cause- DataPipeline uses the
tryParsepattern: a single record failure doesn't affect the entire batch processing
📝 Exercises
- Basic (Difficulty ⭐): Write a function
safeParseInt(String s)that usesint.parseinside a try-catch block. If parsing fails, return0instead of throwing an exception. Test with 3 cases. - Intermediate (Difficulty ⭐⭐): Define a custom exception class
DataFormatExceptionthat carries the field name, invalid value, and line number. Write a CSV parsing function that throws this exception on format errors, and catch it at the call site to print detailed information. - Challenge (Difficulty ⭐⭐⭐): Implement an HTTP request function with a retry mechanism that supports a custom number of retries and a backoff strategy. Retry on
TimeoutException, and after exceeding the retry count, throw an aggregate exception containing all the individual retry exceptions.