Exception Handling — try, catch, throw
Exception Handling — try, catch, throw
No matter how carefully you write code, things can go wrong at runtime — a file might not exist, a network request might fail, or a user might provide invalid input. Exception handling gives you a structured way to deal with these situations gracefully, rather than letting your program crash.
Errors vs Exceptions
Dart distinguishes between two categories of problems:
| Error | Exception | |
|---|---|---|
| Type | Error (and subclasses) | Exception (and subclasses) |
| Meaning | Programming mistake — should not be caught | Expected failure — should be caught and handled |
| Examples | StackOverflowError, OutOfMemoryError, AssertionError | FormatException, IOException, HttpException |
| Should you catch? | ❌ Usually no — fix the bug | ✅ Yes — handle gracefully |
[!IMPORTANT]
Errors indicate bugs in your code that should be fixed, not caught. Exceptions are expected situations that your program should handle. In practice, catch catches both, but be intentional about which you're handling.
The try-catch Block
The basic structure for handling exceptions:
dartvoid main() { try { // Code that might throw int result = 10 ~/ 0; // Integer division by zero print(result); } catch (e) { // Handle the exception print('Something went wrong: \$e'); } print('Program continues...'); // This still runs }
Output:
Something went wrong: IntegerDivisionByZeroException
Program continues...
Without try-catch, the program would have crashed on the division. With it, execution continues after the catch block.
Catching Specific Exceptions with on
Use on ExceptionType to catch only specific exception types:
dartvoid parseNumber(String input) { try { int number = int.parse(input); print('Parsed: \$number'); } on FormatException { print('"\$input" is not a valid integer!'); } } void main() { parseNumber('42'); // Parsed: 42 parseNumber('hello'); // "hello" is not a valid integer! parseNumber('3.14'); // "3.14" is not a valid integer! }
catch with Error Object and Stack Trace
dartvoid main() { try { var list = [1, 2, 3]; print(list[10]); // RangeError } catch (e, s) { // e = the exception object // s = the stack trace (where it happened) print('Error: \$e'); print('Stack trace:\n\$s'); } }
You can also combine on (type filtering) with catch (to get the object):
dartvoid riskyOperation(String input) { try { var number = double.parse(input); var result = 100 / number; print('Result: \$result'); } on FormatException catch (e) { print('Format error: \${e.message}'); } on UnsupportedError catch (e, s) { print('Unsupported: \${e.message}'); print('At: \$s'); } catch (e) { // Catch-all for any other exception print('Unexpected error: \$e'); } } void main() { riskyOperation('25'); // Result: 4.0 riskyOperation('abc'); // Format error: Invalid double riskyOperation('0'); // Result: Infinity (no error) }
[!TIP]
Always put more specific exception types before more general ones. Dart evaluates on clauses top-to-bottom and executes the first match.
The finally Block — Always Runs
The finally block runs regardless of whether an exception was thrown or caught. It's perfect for cleanup code (closing files, releasing connections, etc.):
dartvoid readFile(String path) { print('Opening file...'); try { // Simulating file read if (path.isEmpty) throw ArgumentError('Path cannot be empty'); print('Reading file: \$path'); // ... read operations } catch (e) { print('Error reading file: \$e'); } finally { // This ALWAYS runs — good for cleanup print('Closing file handle...'); } } void main() { readFile('data.txt'); print('---'); readFile(''); // throws }
Output:
Opening file...
Reading file: data.txt
Closing file handle...
---
Opening file...
Error reading file: Invalid argument(s): Path cannot be empty
Closing file handle...
[!NOTE]
finally also runs even if the catch block re-throws. This guarantees cleanup happens no matter what.
The throw Keyword
Use throw to signal that something has gone wrong:
dartdouble divide(double a, double b) { if (b == 0) { throw ArgumentError('Cannot divide by zero'); } return a / b; } void validateAge(int age) { if (age < 0 || age > 150) { throw RangeError.range(age, 0, 150, 'age', 'Age must be between 0 and 150'); } } void main() { print(divide(10, 2)); // 5.0 try { divide(5, 0); } catch (e) { print(e); // Invalid argument(s): Cannot divide by zero } try { validateAge(-5); } on RangeError catch (e) { print(e.message); // Age must be between 0 and 150 } }
You can throw any non-null object in Dart, but it's best practice to throw Exception or Error subclasses.
Creating Custom Exceptions
Define your own exception types to make errors meaningful and domain-specific:
dart// Basic custom exception class AppException implements Exception { final String message; final String? code; const AppException(this.message, {this.code}); String toString() => code != null ? 'AppException [\$code]: \$message' : 'AppException: \$message'; } // Specific exception types class ValidationException extends AppException { final String field; const ValidationException(this.field, String message) : super(message, code: 'VALIDATION_ERROR'); String toString() => 'ValidationException on "\$field": \$message'; } class NetworkException extends AppException { final int? statusCode; const NetworkException(String message, {this.statusCode}) : super(message, code: 'NETWORK_ERROR'); String toString() => statusCode != null ? 'NetworkException (\$statusCode): \$message' : 'NetworkException: \$message'; } class NotFoundException extends AppException { final String resource; const NotFoundException(this.resource) : super('\$resource not found', code: 'NOT_FOUND'); }
Using custom exceptions:
dartclass UserService { final Map<int, String> _users = {1: 'Alice', 2: 'Bob'}; String getUser(int id) { if (id <= 0) { throw ValidationException('id', 'User ID must be positive'); } final user = _users[id]; if (user == null) { throw NotFoundException('User with id \$id'); } return user; } } void main() { var service = UserService(); try { print(service.getUser(1)); // Alice print(service.getUser(99)); // throws NotFoundException } on NotFoundException catch (e) { print(e); // AppException [NOT_FOUND]: User with id 99 not found } on ValidationException catch (e) { print(e); } try { service.getUser(-1); // throws ValidationException } on ValidationException catch (e) { print(e); // ValidationException on "id": User ID must be positive } }
Rethrowing Exceptions
Sometimes you want to catch an exception (to log it or add context), then re-throw it for the caller to handle:
dartvoid processData(String data) { try { var value = int.parse(data); print('Processing: \$value'); } on FormatException catch (e) { print('Logging error: \$e'); // Log it rethrow; // Re-throw the same exception } } void main() { try { processData('not-a-number'); } on FormatException catch (e) { print('Caller caught: \${e.message}'); } }
Output:
Logging error: FormatException: Invalid radix-10 number (at character 1)
Caller caught: Invalid radix-10 number (at character 1)
Use rethrow (not throw e) to preserve the original stack trace.
Common Built-in Exceptions
| Exception | When It's Thrown |
|---|---|
FormatException | Parsing fails (int.parse('abc')) |
RangeError | Index out of bounds, value out of range |
StateError | Object is in wrong state (list.first on empty list) |
UnsupportedError | Method not implemented/supported |
ArgumentError | Invalid argument passed to a function |
TypeError | Type cast fails at runtime |
IOException | File/network I/O failures |
HttpException | HTTP-related errors |
TimeoutException | Operation exceeded time limit |
Real Example: File Parsing with Error Handling
dartimport 'dart:io'; import 'dart:convert'; class CsvParseException extends AppException { final int lineNumber; const CsvParseException(this.lineNumber, String message) : super(message, code: 'CSV_PARSE_ERROR'); String toString() => 'CsvParseException at line \$lineNumber: \$message'; } class DataRecord { final int id; final String name; final double value; DataRecord({required this.id, required this.name, required this.value}); String toString() => 'DataRecord(id: \$id, name: \$name, value: \$value)'; } List<DataRecord> parseCsvFile(String path) { final records = <DataRecord>[]; File file; try { file = File(path); } catch (e) { throw AppException('Cannot access file: \$path'); } try { final lines = file.readAsLinesSync(); for (var i = 1; i < lines.length; i++) { // Skip header final line = lines[i].trim(); if (line.isEmpty) continue; final parts = line.split(','); if (parts.length != 3) { throw CsvParseException(i + 1, 'Expected 3 columns, got \${parts.length}'); } final id = int.tryParse(parts[0].trim()); if (id == null) { throw CsvParseException(i + 1, 'Invalid ID: "\${parts[0].trim()}"'); } final name = parts[1].trim(); if (name.isEmpty) { throw CsvParseException(i + 1, 'Name cannot be empty'); } final value = double.tryParse(parts[2].trim()); if (value == null) { throw CsvParseException(i + 1, 'Invalid value: "\${parts[2].trim()}"'); } records.add(DataRecord(id: id, name: name, value: value)); } } on CsvParseException { rethrow; // Let caller handle parse errors } on FileSystemException catch (e) { throw AppException('File error: \${e.message}'); } finally { print('Parsing complete. Processed \${records.length} records.'); } return records; } void main() { try { var records = parseCsvFile('data.csv'); for (var r in records) { print(r); } } on CsvParseException catch (e) { print('Parse error: \$e'); } on AppException catch (e) { print('App error: \$e'); } catch (e) { print('Unexpected error: \$e'); } }
Summary
| Concept | Syntax / Keyword | Purpose |
|---|---|---|
| try-catch | try { } catch (e) { } | Wrap risky code, handle errors |
| Specific catch | on FormatException | Catch only a specific type |
| Error + trace | catch (e, s) | Get the object and stack trace |
| finally | finally { } | Always-run cleanup code |
| throw | throw SomeException() | Signal that something went wrong |
| rethrow | rethrow | Re-throw current exception, preserve stack |
| Custom exception | implements Exception | Domain-specific error types |
[!TIP]
Design your exception hierarchy to match your domain. Don't just throw Exception('something failed') — throw NetworkException, ValidationException, etc. Your callers can then make intelligent decisions about how to handle each case.
In the next lesson, we'll explore Dart's package ecosystem and how to use pub.dev to supercharge your projects.