Lesson 18 min

Exception Handling — try, catch, throw

00:00 / 00:00

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:

ErrorException
TypeError (and subclasses)Exception (and subclasses)
MeaningProgramming mistake — should not be caughtExpected failure — should be caught and handled
ExamplesStackOverflowError, OutOfMemoryError, AssertionErrorFormatException, 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:

dart
void 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:

dart
void 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

dart
void 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):

dart
void 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.):

dart
void 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:

dart
double 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:

dart
class 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:

dart
void 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

ExceptionWhen It's Thrown
FormatExceptionParsing fails (int.parse('abc'))
RangeErrorIndex out of bounds, value out of range
StateErrorObject is in wrong state (list.first on empty list)
UnsupportedErrorMethod not implemented/supported
ArgumentErrorInvalid argument passed to a function
TypeErrorType cast fails at runtime
IOExceptionFile/network I/O failures
HttpExceptionHTTP-related errors
TimeoutExceptionOperation exceeded time limit

Real Example: File Parsing with Error Handling

dart
import '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

ConceptSyntax / KeywordPurpose
try-catchtry { } catch (e) { }Wrap risky code, handle errors
Specific catchon FormatExceptionCatch only a specific type
Error + tracecatch (e, s)Get the object and stack trace
finallyfinally { }Always-run cleanup code
throwthrow SomeException()Signal that something went wrong
rethrowrethrowRe-throw current exception, preserve stack
Custom exceptionimplements ExceptionDomain-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.

WhatsApp