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


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.

DART
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();
}
TEXT 📖 Display only
> **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


3. Exception vs Error

(1) Semantic Difference

100%
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]
TEXT 📖 Display only
> **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
⚠️ Note: Do not catch Errors. An Error indicates the program state is corrupted; catching it and continuing execution could lead to more severe problems. Only catch Exceptions.


4. try / on / catch / finally

(1) Complete Syntax

▶ Example

TEXT 📖 Display only
> **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

DART
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');
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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');
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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');
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
// 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';
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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');
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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);
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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);
}
TEXT 📖 Display only
> **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

DART
// ============================================
// 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');
}
TEXT 📖 Display only
> **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:

TEXT 📖 Display only
=== 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 on and catch? A: on Type catches a specific exception type without binding a variable (unless catch is added). catch (e) catches any exception and binds it to a variable. Usually, the combination on Type catch (e) is used.

Q: What is the use of catch and rethrow? A: catch allows you to log, perform cleanup, etc., after catching an exception. Then use rethrow to re-throw the same exception so upper layers can continue handling it. rethrow preserves the original stack trace, which is better than throw e.

Q: When does the finally block execute? A: The finally block always executes, regardless of whether an exception is thrown in the try block, whether it's caught, or whether there is a return statement. 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 Exception rather than extend Exception. implements is 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 catch block is a code smell. At least log the exception, or rethrow. If you must ignore it, use catch (_) {} and add a comment explaining why.


📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Write a function safeParseInt(String s) that uses int.parse inside a try-catch block. If parsing fails, return 0 instead of throwing an exception. Test with 3 cases.
  2. Intermediate (Difficulty ⭐⭐): Define a custom exception class DataFormatException that 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.
  3. 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.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏