πŸ›‘οΈ

Dart Exception Handling

try-catch-finally, on-type handling, rethrow

Dart distinguishes recoverable Exceptions from unrecoverable Errors. try-catch-finally handles exceptions, with on-clauses for type-specific branching. rethrow preserves the original stack trace while propagating upward, and async code uses try-catch or Future.catchError() for the same handling.

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; });

Implementation Steps

1

try-catch-finally β€” execute in try, handle in catch, cleanup in finally (close files, disconnect)

2

on-type handling β€” branch by exception type with on FormatException catch (e)

3

rethrow β€” log in catch then rethrow to preserve original stack trace (unlike throw e)

4

Async exceptions β€” use try-catch with async/await, .catchError() for Future chains

Pros

  • Fine-grained handling by exception type with on clause
  • rethrow preserves original stack trace while propagating

Cons

  • Excessive try-catch reduces code readability

Use Cases

Showing error messages to users on API call failure Guaranteeing resource cleanup in file/DB operations with finally