Skip to content

Error Handling

1) Catch specific errors, not catch (e)

  • Prefer on SomeException catch (e, st) over a generic catch.
  • Reason: a generic catch often hides unexpected bugs (null errors, logic issues) and makes debugging harder.

2) Always capture stack trace

  • Use catch (e, st) and log both.
  • Common mistake: logging only e.toString() → you lose the real location of the crash.

3) Don’t swallow errors

  • Common mistake: returning null / empty list on failure without telling caller.
  • Instead: either rethrow or convert to a typed exception and let upper layer decide.

4) Create your own exception types for known failures

  • Make an AppException base class + specific ones (e.g., NetworkException, UnauthorizedException, ParsingException).
  • Benefit: UI/business layer can handle meaningfully (show correct message, retry, logout, etc.).

Example — API call (Bad vs Good)

Bad: generic catch + swallow + no stack trace

dart

Future<User?> fetchUser() async {
try {
final res = await dio.get('/user');
return User.fromJson(res.data);
} catch (e) {
print('error: $e'); // no stack trace, poor logging
return null; // swallows error, caller can’t react properly
}
}

Good: map known errors to custom exceptions + keep stack trace

dart

abstract class AppException implements Exception {
final String message;
final Object? cause;
final StackTrace? stackTrace;
const AppException(this.message, {this.cause, this.stackTrace});
}
class NetworkException extends AppException {
const NetworkException(super.message, {super.cause, super.stackTrace});
}
class UnauthorizedException extends AppException {
const UnauthorizedException(super.message, {super.cause, super.stackTrace});
}
class ServerException extends AppException {
const ServerException(super.message, {super.cause, super.stackTrace});
}
Future<User> fetchUser() async {
try {
final res = await dio.get('/user');
return User.fromJson(res.data);
} on DioException catch (e, st) {
// Map known Dio failures to your app exceptions
final status = e.response?.statusCode;
if (status == 401) {
throw UnauthorizedException('Session expired', cause: e, stackTrace: st);
}
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
throw NetworkException('Connection timeout', cause: e, stackTrace: st);
}
throw ServerException('Request failed', cause: e, stackTrace: st);
}
}

UI layer handling becomes clean and intentional:

dart

try {
final user = await repo.fetchUser();
} on UnauthorizedException {
// redirect to login
} on NetworkException {
// show retry
} on AppException catch (e) {
// show e.message
}