Dart例外処理
try-catch-finally、onタイプ別処理、rethrow
Dartは復旧可能なExceptionと復旧不可のErrorを区別します。try-catch-finallyで例外処理し、on節でタイプ別分岐が可能です。rethrowは元のスタックトレースを維持しながら上位に伝播でき、非同期コードでもtry-catchやFuture.catchError()で同様に処理します。
Exception vs Error
Dart는 Exception(복구 가능한 오류)과 Error(프로그래밍/시스템 오류, 복구 불가)를 구분합니다.
Exception (복구 가능)
FormatException, StateError, TypeError, ArgumentError, RangeError, TimeoutException
Error (복구 불가)
AssertionError, NoSuchMethodError, StackOverflowError, OutOfMemoryError
기본 try-catch-finally
try 블록에서 실행하고, catch에서 예외를 처리하며, finally는 항상 실행됩니다.
try {
int result = 12 ~/ 0;
print('결과: \$result');
} catch (e) {
print('예외 발생: \$e');
} finally {
print('finally 블록 실행');
}
on 절로 타입별 처리
on 타입 catch (e)으로 예외 종류별로 분기할 수 있습니다. 마지막 catch (e, s)는 나머지 모든 예외를 잡으며 스택 트레이스도 받습니다.
try {
dynamic value = 'not a number';
int number = int.parse(value);
} on FormatException catch (e) {
print('숫자로 변환할 수 없음: \$e');
} on TypeError catch (e) {
print('타입 오류 발생: \$e');
} catch (e, s) {
print('기타 예외: \$e');
print('스택 트레이스: \$s');
}
rethrow — 원래 스택 트레이스 보존
catch에서 로깅 후 rethrow로 상위에 전파합니다. throw e와 달리 원래 스택 트레이스가 유지됩니다.
void processFile(String filename) {
try {
var file = File(filename);
var contents = file.readAsStringSync();
} catch (e) {
print('파일 처리 중 오류: \$e');
rethrow; // 원래 스택 트레이스 유지
}
}
void main() {
try {
processFile('존재하지_않는_파일.txt');
} catch (e) {
print('메인에서 오류 처리: \$e');
}
}
커스텀 Exception 클래스
implements Exception으로 도메인별 예외를 정의하면 의미 있는 에러 메시지와 컨텍스트를 전달할 수 있습니다.
class InsufficientBalanceException implements Exception {
final double balance;
final double withdrawal;
InsufficientBalanceException(this.balance, this.withdrawal);
@override
String toString() {
return '잔액 부족: 현재 \$balance, 출금 요청 \$withdrawal';
}
}
// 사용
try {
account.withdraw(1500);
} on InsufficientBalanceException catch (e) {
print('출금 실패: \$e');
} on ArgumentError catch (e) {
print('인수 오류: \$e');
}
비동기 예외 — async/await
async 함수에서는 동기 코드와 동일하게 try-catch-finally를 사용합니다.
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
throw Exception('데이터를 가져올 수 없습니다.');
}
Future<void> processData() async {
try {
String data = await fetchData();
print('데이터: \$data');
} catch (e) {
print('오류 발생: \$e');
} finally {
print('데이터 처리 완료');
}
}
비동기 예외 — Future 체인
Future 체인에서는 .catchError()와 .whenComplete()를 사용합니다. test: 파라미터로 특정 타입만 선택적으로 처리할 수도 있습니다.
fetchData()
.then((data) => print('데이터: \$data'))
.catchError((e) => print('오류 발생: \$e'))
.whenComplete(() => print('작업 완료'));
// 특정 타입만 선택 처리
processTask()
.catchError(
(e) => print('타임아웃: \$e'),
test: (e) => e is TimeoutException,
)
.catchError((e) => print('기타 오류: \$e'));
Stream 예외 처리
Stream은 3가지 방식으로 예외를 처리할 수 있습니다.
1. await for + try-catch
try {
await for (var number in countStream(5)) {
print('숫자: \$number');
}
} catch (e) {
print('스트림 오류: \$e');
}
2. listen의 onError 콜백
countStream(5).listen(
(data) => print('숫자: \$data'),
onError: (e) => print('오류: \$e'),
onDone: () => print('완료'),
cancelOnError: false, // false면 오류 후에도 계속
);
3. handleError 메서드
generateNumbers()
.handleError((error) => print('처리: \$error'))
.listen((data) => print('데이터: \$data'));
Zone — 글로벌 에러 핸들링
runZonedGuarded는 Zone 내의 모든 비동기 에러를 잡아냅니다. Flutter 앱의 글로벌 에러 핸들러로 활용됩니다.
import 'dart:async';
runZonedGuarded(
() {
Future.delayed(Duration(seconds: 1), () {
throw Exception('비동기 오류');
});
},
(error, stack) {
print('Zone에서 캐치: \$error');
},
);
Flutter 에러 처리 패턴
Flutter 앱에서는 FlutterError.onError와 runZonedGuarded를 조합하여 모든 예외를 포착합니다.
void main() {
// Flutter 프레임워크 에러
FlutterError.onError = (details) {
if (kReleaseMode) {
Zone.current.handleUncaughtError(
details.exception, details.stack!);
} else {
FlutterError.dumpErrorToConsole(details);
}
};
// 그 외 모든 비동기 에러
runZonedGuarded(
() => runApp(MyApp()),
(error, stackTrace) {
print('예기치 않은 오류: \$error');
},
);
}
FutureBuilder / StreamBuilder 에러 처리
위젯에서 비동기 데이터를 표시할 때 snapshot.hasError로 에러 상태를 체크합니다.
FutureBuilder<String>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('오류: \${snapshot.error}');
} else if (snapshot.hasData) {
return Text('데이터: \${snapshot.data}');
}
return Text('데이터 없음');
},
)
Best Practices
-
✓
구체적인 예외 타입 사용 — catch-all 대신 on FormatException, on HttpException 등으로 명확하게 분기
-
✓
finally로 리소스 정리 — 파일, DB 연결 등을 finally에서 반드시 해제
-
✓
예외 래핑으로 컨텍스트 전달 — 상위로 전파할 때 원인(cause)을 포함한 커스텀 예외로 감싸기
-
✓
예외는 예외적 상황에만 — 흐름 제어 용도로 사용하지 말고 -1, null 등 반환값 활용
-
✓
중앙화된 에러 핸들러 — ErrorHandler.guard() 패턴으로 로깅과 에러 처리를 통일
중앙화된 에러 핸들러 패턴
class ErrorHandler {
static void logError(Object error, StackTrace stackTrace) {
print('ERROR: \$error');
print('STACK: \$stackTrace');
}
static Future<T> guard<T>(Future<T> Function() fn) async {
try {
return await fn();
} catch (error, stackTrace) {
logError(error, stackTrace);
rethrow;
}
}
}
// 사용
await ErrorHandler.guard(() async {
final data = await fetchData();
return data;
});
実装ステップ
try-catch-finally — tryで実行、catchで処理、finallyでリソース整理(ファイルクローズ、接続解除等)
onタイプ別処理 — on FormatException catch (e)で例外種類別分岐
rethrow — catchでログ後rethrowで上位に伝播、throw eと違い元のスタックトレースを維持
非同期例外 — async/awaitではtry-catchそのまま使用、Futureチェーンでは.catchError()活用
メリット
- ✓ on節で例外タイプ別の細かい処理が可能
- ✓ rethrowで元のスタックトレースを保存しながら伝播
デメリット
- ✗ 過度なtry-catchはコード可読性を低下