| |

Dart 34 🎯 Exception Handling: try, catch, on, finally, and Custom Exceptions

An exception is a signal that something went wrong. It interrupts the normal flow of the program, unwinds the stack, and transfers control to the nearest handler. Dart’s exception system has four keywords: try, catch, on, and finally. The try block contains the code that might throw. The catch block handles the exception. The on clause filters by exception type. The finally block runs whether or not an exception occurred. Custom exceptions are classes that extend Exception or implement Error, and they carry the information the handler needs.

Key point: An exception is an object that is thrown with throw and caught with try/catch. The on Type clause catches only the specified type. The catch (e) clause catches everything and binds the exception to a variable. The finally block runs after the try and catch, regardless of the outcome. Custom exceptions are classes; they can extend Exception, implement Exception, or extend Error. The distinction between Exception and Error is a convention: Exception is for conditions the program should catch and handle; Error is for conditions the program should not catch, such as a null dereference or an index out of range.


Why exception handling matters

The error-propagation problem. A function that can fail has two options: return a sentinel value that indicates failure, or throw an exception. The sentinel approach requires the caller to check every return value, and forgetting the check is a silent bug. The exception approach propagates the failure automatically. The caller that can handle it catches it; the callers that cannot are skipped, and the stack unwinds to the handler.

The specificity problem. A handler that catches everything catches too much. It catches the exception it was written for and the exceptions it was not. The on clause filters by type, so the handler catches only the exception it can handle. The catch clause catches the rest.

The cleanup problem. A function that opens a file must close it. A function that acquires a lock must release it. The finally block runs whether the function succeeded, failed, or returned early. It is the place for the cleanup that must happen in every path.

The information problem. An exception carries the information about the failure: what went wrong, where, and with what data. A custom exception class carries the fields the handler needs. A generic Exception carries nothing.

The distinction problem. Dart distinguishes Exception from Error. An Exception is a condition the program should handle: a file not found, a network timeout, a validation failure. An Error is a condition the program should not handle: a null dereference, an index out of range, a type cast failure. The convention is to catch Exception and let Error propagate.

The performance problem. Exceptions are for exceptional conditions, not for control flow. A loop that throws an exception on every iteration is slower than a loop with a condition. The exception mechanism is designed for the rare path, and using it for the common path is a misuse.


a. The try, catch, and on clauses

The try block contains the code that might throw. The catch block handles the exception.

try {
  var result = 10 ~/ 0;
  print(result);
} catch (e) {
  print('Caught: $e');
}

The 10 ~/ 0 throws an IntegerDivisionByZeroException (or an UnsupportedError in some contexts). The catch (e) catches it and binds it to e. The print runs.

The on clause filters by type.

try {
  var result = 10 ~/ 0;
} on IntegerDivisionByZeroException {
  print('Division by zero');
} catch (e) {
  print('Other error: $e');
}

The on IntegerDivisionByZeroException catches only that type. The catch (e) catches everything else. The clauses are evaluated in order, and the first match wins.

The on clause can bind the exception to a variable.

try {
  var result = 10 ~/ 0;
} on IntegerDivisionByZeroException catch (e) {
  print('Caught: $e');
}

The catch (e) after the on Type binds the exception. The on Type catch (e) is the combination.

The catch clause can bind the stack trace.

try {
  throw Exception('Something went wrong');
} catch (e, stackTrace) {
  print('Exception: $e');
  print('Stack trace: $stackTrace');
}

The second parameter of catch is the StackTrace. It is useful for logging.

The on clause can be combined with the stack trace.

try {
  throw FormatException('Invalid format');
} on FormatException catch (e, stackTrace) {
  print('Format error: ${e.message}');
  print(stackTrace);
}

The on FormatException catch (e, stackTrace) catches the specific type and binds both the exception and the stack trace.

A try block can have multiple on clauses.

try {
  riskyOperation();
} on FormatException catch (e) {
  print('Format: ${e.message}');
} on IOException catch (e) {
  print('IO: $e');
} catch (e) {
  print('Other: $e');
}

The clauses are evaluated in order. The first matching type is used. The catch (e) at the end catches anything that is not a FormatException or an IOException.

The rethrow keyword rethrows the current exception from inside a catch block.

try {
  riskyOperation();
} catch (e) {
  print('Logging: $e');
  rethrow; // rethrow the same exception
}

The rethrow preserves the original stack trace. The throw e would replace it with a new stack trace. The rethrow is the correct way to log and propagate.


b. The finally clause and the order of execution

The finally block runs after the try and any catch, regardless of whether an exception occurred.

try {
  print('Try');
  throw Exception('Error');
} catch (e) {
  print('Catch: $e');
} finally {
  print('Finally');
}

The output is:

Try
Catch: Exception: Error
Finally

The finally runs after the catch. It runs whether or not the exception was caught.

The finally runs even if the try block returns.

int example() {
  try {
    return 1;
  } finally {
    print('Finally');
  }
}

print(example());

The output is:

Finally
1

The finally runs before the return value is delivered. The return 1 is evaluated, the finally runs, and then the value is returned.

The finally runs even if the exception is not caught.

try {
  throw Exception('Error');
} finally {
  print('Finally');
}

The finally runs, and then the exception propagates. The output is:

Finally
Unhandled exception: Exception: Error

The finally is the place for the cleanup that must happen in every path: closing a file, releasing a lock, stopping a timer.

var file = File('data.txt').openSync();
try {
  // use the file
} finally {
  file.closeSync();
}

The file is closed whether the try block succeeds, fails, or returns.

The finally can itself throw an exception. If it does, the new exception replaces the original.

try {
  throw Exception('Original');
} finally {
  throw Exception('Finally');
}

The output is:

Unhandled exception: Exception: Finally

The original exception is lost. The finally should not throw unless the new exception is the intended result.

The try/finally without a catch is valid. It does not catch the exception; it only ensures the cleanup.

try {
  riskyOperation();
} finally {
  cleanup();
}

The exception propagates after the finally runs. The pattern is used when the caller is responsible for the handling, and the current function is only responsible for the cleanup.


c. Custom exceptions

A custom exception is a class. It can extend Exception, implement Exception, or extend Error.

class ValidationException implements Exception {
  final String message;
  final String field;

  ValidationException(this.message, this.field);

  @override
  String toString() => 'ValidationException: $message (field: $field)';
}

The ValidationException implements Exception and carries the message and the field. The toString is overridden for a readable representation.

The exception is thrown with throw.

void validateEmail(String email) {
  if (!email.contains('@')) {
    throw ValidationException('Invalid email', 'email');
  }
}

The throw takes an instance of the exception. The exception is caught with on.

try {
  validateEmail('invalid');
} on ValidationException catch (e) {
  print('${e.field}: ${e.message}');
}

The on ValidationException catch (e) catches the custom exception and accesses its fields.

A custom exception can extend another exception.

class NetworkException implements Exception {
  final String message;
  final int statusCode;

  NetworkException(this.message, this.statusCode);

  @override
  String toString() => 'NetworkException($statusCode): $message';
}

class TimeoutException extends NetworkException {
  TimeoutException() : super('Request timed out', 408);
}

The TimeoutException extends NetworkException and provides a default message and status code. It is caught by on NetworkException because it is a subtype.

A custom exception can be sealed or final, which allows exhaustive switch.

sealed class AppException implements Exception {
  const AppException();
}

final class NotFoundException extends AppException {
  final String resource;
  const NotFoundException(this.resource);
}

final class PermissionException extends AppException {
  final String action;
  const PermissionException(this.action);
}

The AppException is a sealed class, and the switch over it is exhaustive.

try {
  riskyOperation();
} on AppException catch (e) {
  switch (e) {
    case NotFoundException(resource: var r):
      print('Not found: $r');
    case PermissionException(action: var a):
      print('Permission denied: $a');
  }
}

The switch handles every subtype. Adding a new subtype turns the switch into a compile error.

The distinction between Exception and Error: Exception is for conditions the program should catch and handle. Error is for conditions the program should not catch, such as a null dereference or an index out of range. The convention is to catch Exception and let Error propagate.

try {
  riskyOperation();
} on Exception catch (e) {
  print('Handled: $e');
}
// Error is not caught and propagates

The on Exception catches the exceptions but not the errors. The errors propagate to the top level, where the program crashes with a stack trace.

A custom error extends Error.

class ConfigurationError extends Error {
  final String message;
  ConfigurationError(this.message);

  @override
  String toString() => 'ConfigurationError: $message';
}

The ConfigurationError extends Error. It is thrown for the conditions that indicate a programming mistake, not a runtime failure the program can recover from.


Complete Example Session

// ============================================
// PART 1: BASIC TRY/CATCH
// ============================================
try {
  var result = 10 ~/ 0;
} catch (e) {
  print('Caught: $e');
}
// ============================================
// PART 2: ON CLAUSE
// ============================================
try {
  throw FormatException('Invalid');
} on FormatException catch (e) {
  print('Format: ${e.message}');
}
// ============================================
// PART 3: MULTIPLE ON CLAUSES
// ============================================
try {
  riskyOperation();
} on FormatException catch (e) {
  print('Format: ${e.message}');
} on IOException catch (e) {
  print('IO: $e');
} catch (e) {
  print('Other: $e');
}
// ============================================
// PART 4: STACK TRACE
// ============================================
try {
  throw Exception('Error');
} catch (e, stackTrace) {
  print('$e');
  print('$stackTrace');
}
// ============================================
// PART 5: FINALLY
// ============================================
try {
  print('Try');
  throw Exception('Error');
} catch (e) {
  print('Catch');
} finally {
  print('Finally');
}
// ============================================
// PART 6: FINALLY WITH RETURN
// ============================================
int example() {
  try {
    return 1;
  } finally {
    print('Finally');
  }
}
// ============================================
// PART 7: TRY/FINALLY WITHOUT CATCH
// ============================================
try {
  riskyOperation();
} finally {
  cleanup();
}
// ============================================
// PART 8: RETHROW
// ============================================
try {
  riskyOperation();
} catch (e) {
  print('Logging: $e');
  rethrow;
}
// ============================================
// PART 9: CUSTOM EXCEPTION
// ============================================
class ValidationException implements Exception {
  final String message;
  final String field;
  ValidationException(this.message, this.field);

  @override
  String toString() => 'ValidationException: $message (field: $field)';
}

void validateEmail(String email) {
  if (!email.contains('@')) {
    throw ValidationException('Invalid email', 'email');
  }
}

try {
  validateEmail('invalid');
} on ValidationException catch (e) {
  print('${e.field}: ${e.message}');
}
// ============================================
// PART 10: SEALED EXCEPTION
// ============================================
sealed class AppException implements Exception {
  const AppException();
}

final class NotFoundException extends AppException {
  final String resource;
  const NotFoundException(this.resource);
}

final class PermissionException extends AppException {
  final String action;
  const PermissionException(this.action);
}

try {
  riskyOperation();
} on AppException catch (e) {
  switch (e) {
    case NotFoundException(resource: var r):
      print('Not found: $r');
    case PermissionException(action: var a):
      print('Permission denied: $a');
  }
}

The ten parts covered basic try/catch, the on clause, multiple on clauses, the stack trace, finally, finally with a return, try/finally without a catch, rethrow, the custom exception, and the sealed exception.


Quick Reference

Exception Keywords

KeywordPurpose
tryCode that might throw
catchHandles the exception
onFilters by type
finallyRuns in every path
throwThrows an exception
rethrowRethrows the current exception

Try Block Forms

FormCatches
try { } catch (e) { }Everything
try { } on Type { }Specific type
try { } on Type catch (e) { }Specific type, bound
try { } catch (e, s) { }Exception and stack trace
try { } finally { }Nothing, cleanup only
try { } catch (e) { } finally { }Everything, with cleanup

Exception vs Error

AspectExceptionError
PurposeRecoverable conditionProgramming mistake
Should catchYesNo
ExamplesFormatException, IOExceptionArgumentError, StateError
Base classExceptionError

Custom Exception

FormExample
Implementclass MyException implements Exception
Extendclass MyException extends SomeException
Fieldsfinal String message;
toString@override String toString() => ...
Sealedsealed class AppException implements Exception

Best Practices

✅ Do This:

// Catch specific types
try { } on FormatException catch (e) { }                       // ✅

// Use finally for cleanup
try { } finally { resource.close(); }                          // ✅

// Use rethrow to preserve the stack trace
catch (e) { log(e); rethrow; }                                 // ✅

// Create custom exceptions with fields
class AppException implements Exception {
  final String message;
  AppException(this.message);
}                                                              // ✅

// Use sealed exceptions for exhaustive handling
sealed class AppException implements Exception { }             // ✅

// Bind the stack trace for logging
catch (e, stackTrace) { log(e, stackTrace); }                  // ✅

❌ Don’t Do This:

// Don't catch everything without a reason
try { } catch (e) { }  // swallows Error too                      // ⚠️

// Don't use exceptions for control flow
try {
  for (var i = 0; ; i++) {
    if (list[i] == target) break;
  }
} catch (e) { }  // IndexError as control flow                    // ❌

// Don't throw in finally
try { } finally { throw Exception('Finally'); }                   // ❌

// Don't catch Error
try { } on Error catch (e) { }                                    // ⚠️

// Don't use a generic Exception when a custom one fits
throw Exception('Invalid email');                                 // ⚠️

// Don't forget the stack trace when logging
catch (e) { log(e); }  // no stack trace                          // ⚠️

// Don't ignore the caught exception silently
try { } catch (e) { }  // nothing                                  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Catching too muchcatch (e) without onUse on Type
Finally throwsNew exception replaces originalDon’t throw in finally
Stack trace lostthrow e in catchUse rethrow
Custom exception not caughtWrong type in onCheck the type
Exception swallowedEmpty catchLog or handle
Error caughton ErrorLet Error propagate
Control flow via exceptionsUsing exceptions for loopsUse conditions

Real-World Examples

1. Basic Try/Catch

try {
  var result = 10 ~/ 0;
} catch (e) {
  print('Error: $e');
}

2. Specific Type

try {
  jsonDecode('invalid');
} on FormatException catch (e) {
  print('Invalid JSON: ${e.message}');
}

3. Cleanup with Finally

var file = File('data.txt').openSync();
try {
  // use the file
} finally {
  file.closeSync();
}

4. Rethrow

try {
  riskyOperation();
} catch (e) {
  logger.error(e);
  rethrow;
}

5. Custom Exception

class ValidationException implements Exception {
  final String message;
  final String field;
  ValidationException(this.message, this.field);
}

6. Sealed Exception

sealed class AppException implements Exception { }
final class NotFoundException extends AppException { }
final class PermissionException extends AppException { }

7. Exhaustive Switch

switch (e) {
  case NotFoundException(resource: var r): print('Not found: $r');
  case PermissionException(action: var a): print('Denied: $a');
}

8. Multiple Types

try {
  riskyOperation();
} on FormatException catch (e) {
  // handle format
} on IOException catch (e) {
  // handle IO
}

9. Stack Trace

try {
  riskyOperation();
} catch (e, stackTrace) {
  print('$e\n$stackTrace');
}

10. Try/Finally Without Catch

try {
  riskyOperation();
} finally {
  cleanup();
}

Visual

Try/Catch/Finally Flow

┌─────────────────────────────────────────────────────────────┐
│  TRY                                                        │
│    │                                                        │
│    ├── Success ────────────────┐                            │
│    │                            │                           │
│    ├── Exception thrown         │                           │
│    │     │                      │                           │
│    │     ▼                      │                           │
│    │  CATCH                     │                           │
│    │     │                      │                           │
│    │     ├── Handle ────────────┤                           │
│    │     │                      │                           │
│    │     └── Rethrow            │                           │
│    │          │                 │                           │
│    │          ▼                 │                           │
│    │     FINALLY ◀──────────────┘                           │
│    │          │                                             │
│    │          ▼                                             │
│    │     Continue or propagate                              │
│    │                                                        │
│    └── FINALLY (if no exception)                            │
│          │                                                  │
│          ▼                                                  │
│       Continue                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Exception vs Error

┌─────────────────────────────────────────────────────────────┐
│  EXCEPTION                                                  │
│                                                             │
│  Conditions the program can handle                          │
│  • FormatException                                          │
│  • IOException                                              │
│  • TimeoutException                                         │
│                                                             │
│  Catch these.                                               │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ERROR                                                      │
│                                                             │
│  Programming mistakes                                       │
│  • ArgumentError                                            │
│  • StateError                                               │
│  • IndexError                                               │
│  • AssertionError                                           │
│                                                             │
│  Let these propagate.                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Custom Exception Hierarchy

┌─────────────────────────────────────────────────────────────┐
│  sealed class AppException implements Exception { }         │
│         │                                                   │
│         ├── final class NotFoundException extends AppException│
│         │     resource: String                              │
│         │                                                   │
│         ├── final class PermissionException extends AppException│
│         │     action: String                                │
│         │                                                   │
│         └── final class ValidationException extends AppException│
│               field: String, message: String                │
│                                                             │
│  The switch over AppException is exhaustive.                │
│  Adding a subtype turns incomplete switches into errors.    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Finally Runs in Every Path

┌─────────────────────────────────────────────────────────────┐
│  try { return 1; } finally { print('Finally'); }            │
│                                                             │
│  Output:                                                    │
│    Finally                                                  │
│    1                                                        │
│                                                             │
│  try { throw Exception(); } finally { print('Finally'); }   │
│                                                             │
│  Output:                                                    │
│    Finally                                                  │
│    Unhandled exception                                      │
│                                                             │
│  The finally block is the cleanup that runs in every path.  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
tryCode that might throw
catch (e)Catch everything
on TypeCatch specific type
on Type catch (e)Specific type, bound
catch (e, s)Exception and stack trace
finallyRuns in every path
throwThrow an exception
rethrowRethrow the current exception
CustomClass implements Exception
Sealedsealed class for exhaustive handling
ExceptionRecoverable condition
ErrorProgramming mistake

Key takeaways:

  • try contains the code that might throw; catch handles the exception. The on Type clause filters by type, and the catch (e) clause binds the exception. The clauses are evaluated in order.
  • finally runs in every path. It runs after the try and catch, whether the try succeeded, failed, or returned. It is the place for cleanup: closing a file, releasing a lock, stopping a timer.
  • rethrow preserves the stack trace. The throw e in a catch block creates a new stack trace. The rethrow keeps the original. Use rethrow when logging and propagating.
  • Custom exceptions are classes. They implement Exception or extend another exception. They carry the fields the handler needs. The toString is overridden for a readable message.
  • Sealed exceptions enable exhaustive switch. A sealed class with final subclasses lets the switch handle every case. Adding a new subclass turns the incomplete switch into a compile error.
  • Exception is for handling; Error is for propagating. The convention is to catch Exception and let Error propagate. An Error indicates a programming mistake that should be fixed, not caught.
  • Exceptions are for exceptional conditions. Do not use them for control flow. A loop with a condition is faster and clearer than a loop that throws and catches on every iteration.

Remember: Exception handling is the mechanism for separating the happy path from the error path. The try block is the happy path, the catch block is the error path, and the finally block is the cleanup. The on clause is the filter, and the custom exception is the carrier of the information. Use specific catches, use finally for cleanup, use rethrow to propagate, and use sealed exceptions for the exhaustive handling. The errors that should be handled are handled; the errors that should propagate are propagated. And the cleanup happens in every path.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!