Dart Fundamentals: The Language Behind Flutter · Classes, Modern Dart 3 Features and Async

Errors and Exceptions in Dart

Throw and catch exceptions, use on/catch/finally, define custom exception types, rethrow with context, and know the difference between Exceptions (expected failures) and Errors (bugs).

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A payment validator that throws specific exceptions for different problems, a caller that handles each type differently, and a finally block that always cleans up.

The idea

  • throw SomeException('message') raises; try { } on Type catch (e) { } catch (e, stackTrace) { } finally { } handles.
  • on Type catches a specific type; catch (e) catches anything; finally always runs (close files, hide spinners).
  • rethrow re-raises inside a catch while keeping the original stack trace.
  • Exception is for expected, recoverable problems (bad input, network failures); implement it for your own types. Error (like StateError, ArgumentError, RangeError) signals programming bugs; generally don't catch those, fix them.
  • In async code, try/catch works around await; unawaited futures report errors elsewhere.

Some teams prefer returning a result type (a sealed Result with Ok and Err, as in the previous lesson's patterns) for expected failures, keeping exceptions for truly exceptional cases.

The code

Custom exceptions, on/catch/finally and rethrow
class PaymentException implements Exception {
  final String message;
  const PaymentException(this.message);
  @override
  String toString() => 'PaymentException: $message';
}

class CardDeclined extends PaymentException {
  const CardDeclined() : super('card declined');
}

class InsufficientFunds extends PaymentException {
  final int shortByCents;
  const InsufficientFunds(this.shortByCents) : super('insufficient funds');
}

void charge(int cents, {required int balance, bool blocked = false}) {
  if (cents <= 0) throw ArgumentError.value(cents, 'cents', 'must be positive'); // a bug in the caller
  if (blocked) throw const CardDeclined();
  if (cents > balance) throw InsufficientFunds(cents - balance);
}

String checkout(int cents, {required int balance, bool blocked = false}) {
  var spinner = 'on';
  try {
    charge(cents, balance: balance, blocked: blocked);
    return 'paid ${(cents / 100).toStringAsFixed(2)}';
  } on InsufficientFunds catch (e) {
    return 'short by ${(e.shortByCents / 100).toStringAsFixed(2)}: try a smaller amount';
  } on PaymentException catch (e) {
    return 'payment failed: ${e.message}';
  } finally {
    spinner = 'off'; // always runs, even after return
    print('  (spinner $spinner)');
  }
}

void main() {
  print(checkout(2500, balance: 10000));
  print(checkout(12000, balance: 10000));
  print(checkout(500, balance: 10000, blocked: true));

  try {
    try {
      checkout(-1, balance: 100);
    } catch (e) {
      print('logging: ${e.runtimeType}');
      rethrow; // keep the original error and stack trace
    }
  } on ArgumentError catch (e) {
    print('bug found: ${e.message} (${e.invalidValue})');
  }
}

Output

  (spinner off)
paid 25.00
  (spinner off)
short by 20.00: try a smaller amount
  (spinner off)
payment failed: card declined
  (spinner off)
logging: ArgumentError
bug found: must be positive (-1)

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Catching everything with a bare catch and ignoring it, which hides bugs.
  • Catching Errors like RangeError or StateError; these mean the code is wrong.
  • Throwing strings (throw 'oops'); throw typed exceptions so callers can handle them specifically.
  • throw e instead of rethrow, losing the original stack trace.

Key points

  • try / on Type catch / catch / finally; order handlers from specific to general.
  • Custom exceptions implement Exception; Error subclasses indicate bugs.
  • rethrow preserves the stack trace.
  • Consider sealed result types for expected failures.

Further reading: Error handling.

Practice questions

What's the difference between Exception and Error in Dart?

Answer

Exceptions represent expected, recoverable conditions (invalid input, network failure) that callers may handle. Errors (StateError, ArgumentError, RangeError…) indicate programming bugs that should be fixed rather than caught.

Why use rethrow instead of throw e inside a catch block?

Answer

rethrow re-raises the same exception while preserving its original stack trace; throw e starts a new stack trace from the catch block, losing where the error actually happened.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?