Day 14: Error Handling & Custom Exceptions
Handle failures gracefully and build fault-tolerant apps that NEVER crash mysteriously. Master Error vs Exception vs Result Pattern - the professional approach to robust Flutter development!
မင်္ဂလာပါ.. 👋
100 days of Flutter ရဲ့ Day 14 က ကြိုဆိုပါတယ်။ ဒီနေ့မှာတော့ dart langauge နဲ့ ပတ်သက်တာတွေကို ဆက်လက်ပြီး လေ့လာသွားပါမယ်။ ဒီနေ့ဆွေးနွေးသွားမယ့် ခေါင်းစဥ်တွေကတော့
- Error & Exception
- try/catch/finally mechanics
- Custom Exceptions
- Return Pattern
တွေပဲ ဖြစ်ပါတယ်။
Fundamental Philosophy
ရှေ့ရက်တွေက Dart မှာ ရေးထားတဲ့ code တွေကနေ တခုခု အဆင်မပြေတာတွေ ဖြစ်တဲ့အခါ Error တွေ၊ Exception တွေ မြင်ဖူးပြီးလောက်ပြီ ထင်ပါတယ်။ ဒီတော့ ဘာကွာလဲ ကြည့်ကြည့်ရအောင်ပါ။
Error
Error ဆိုတာကိုတော့ programming bug လို့ ပြောလို့ရပါတယ်။ ဆိုလိုတာကတော့ ရေးထားတဲ့ code ထဲမှာ အမှားတခုခု ရှိနေတာဖြစ်ပါတယ်။ ဒီတော့ အမှားကို ဘာကြောင့် ဖြစ်ရတာလဲ အဖြေရှာပြီး ဖြေရှင်းပေးဖို့ လိုပါလိမ့်မယ်။ ရှေ့ရက်တွေတုန်းက မြင်ခဲ့ရတာတွေဆို RangeError, AssertionError, TypeError စသဖြင့် error တွေ အမျိုးမျိုး ရှိပါတယ်။ ဒါတွေက မဖြစ်သင့်တာတွေ ဖြစ်လို့ ဒီလိုတွေမဖြစ်အောင် logic တွေ သေချာ ထည့်ရေးဖို့ လိုအပ်ပါလိမ့်မယ်။ Error ဖြစ်တဲ့အခါ recover လုပ်ဖို့ဆိုတာထက် ဘာကြောင့်ဖြစ်လဲဆိုတာကို ဖြေရှင်းရမှာမျိုးပါ။
void processAge(int age) {
print("Age is: $age");
}
void main() {
dynamic userInput = "twenty-five"; // Dynamic type
processAge(userInput); // ☠️ TypeError at runtime!
}
// Output:
// Unhandled exception:
// type 'String' is not a subtype of type 'int'အပေါ်မှာ ပြသလိုမျိုး ကြုံတဲ့အခါ try/catch တွေ သုံးပြီး handle လုပ်တာထက် ဒီလို error မဖြစ်အောင် type ကို ကြေငြာတာ၊ စစ်ဆေးတာတွေနဲ့ ကာကွယ်ပေးရမှာမျိုးဖြစ်ပါတယ်။
void main() {
String userInput = "25";
int? age = int.tryParse(userInput); // ✅ Safe parsing
if (age != null) {
processAge(age);
} else {
print("Invalid age format");
}
}Exception
Excpetion ကတော့ environmental failure တွေလို့ ပြောလို့ရပါတယ်။ သက်ဆိုင်ရာ app layer တွေမှာ အဆင်မပြေတာတွေ ဖြစ်တဲ့အခါ အဆင့်ဆင့် သိနိုင်ဖို့ Exception တွေနဲ့ အချက်ပြတာမျိုး ဖြစ်ပါတယ်။ Exception တွေကိုတော့ အခြေအနေအရ အဆင်မပြေတာတွေ ဖြစ်သွားနိုင်တဲ့အခါ အသုံးပြုကြတဲ့အတွက် Exception ဖြစ်တဲ့အခါ recover လုပ်ဖို့ဆိုတာကို ထည့်သွင်းစဥ်းစား ရပါတယ်။
import 'dart:io';
Future<String> readConfig() async {
try {
final file = File('config.json');
return await file.readAsString();
} on FileSystemException catch (e) {
print("Config file not found: ${e.message}");
return '{"theme": "default"}'; // Fallback
}
}
void main() {
readConfig();
}
// Output:
// Config file not found: Cannot open fileဒီမှာကြည့်မယ်ဆို config.json file ကို ဖတ်ဖို့လုပ်တဲ့အခါ file မရှိတာတို့၊ permission မရလို့ ဖတ်မရတာတို့ စသဖြင့် ဖြစ်နိုင်ပါတယ်။ ဒီလိုတွေဖြစ်တာက code ရေးတာ လွဲမှားလို့ဆိုတာထက် file system ထဲကနေ တခုခု မှားယွင်းနေတာမျိုးကြောင့်ပဲ ဖြစ်ပါတယ်။ ဒီတော့ try/catch နဲ့ handle လုပ်နိုင်ပါတယ်။
Try/Catch/Finally Mechanics
အပေါ်မှာ exception ဖြစ်ရင် handle လုပ်တဲ့အခါ try/catch ဆိုတာကို တွေ့ပါလိမ့်မယ်။ ဒါကို အသုံးပြုချင်းအားဖြင့် exception အမျိုးအစားတွေကို ခွဲခြားနိုင်ပြီး သင့်တော်သလို ကိုင်တယ်သွားနိုင်မှာပဲ ဖြစ်ပါတယ်။ တကယ်လို့ exception တခုချင်း စစ်မနေဘဲ ဘာ exception ဖြစ်ဖြစ် ကိုင်တွယ်ချင်တယ်ဆိုလဲ on xxxException ဆိုတာကို ထည့်မနေဘဲ တန်းပြီး catch (e) နဲ့တင် အလုပ်ဖြစ်ပါတယ်။
try/catch မှာ သူတို့နဲ့ တွဲပါတာကတော့ finally keyword ပဲ ဖြစ်ပါတယ်။ သူ့ကိုတော့ try ထဲကဟာ အလုပ်လုပ်ပြီးလို့ပဲ ဖြစ်ဖြစ်၊ exception ဖြစ်လို့ catch ထဲကို ရောက်သွားလို့ပဲ ဖြစ်ဖြစ် အကုန်ပြီးတဲ့အခါ လုပ်ဆောင်စေချင်တာတွေ ရှိရင် အသုံးပြုပါတယ်။ ဘာပဲဖြစ်ဖြစ် နောက်ဆုံးမှာတော့ သူ့ထဲကဟာတွေ အလုပ်လုပ်ပါလို့ ပြောတာနဲ့ တူပါတယ်။
void main() {
print("1. Main starts");
try {
print("2. Entering try block");
levelOne();
print("7. This NEVER executes if exception is thrown");
} catch (e) {
print("8. Caught in main: $e");
} finally {
print("9. Finally in main always runs");
}
print("10. Main continues");
}
void levelOne() {
print("3. Level one starts");
levelTwo();
print("6. This NEVER executes if exception is thrown");
}
void levelTwo() {
print("4. Level two starts");
throw Exception("Something went wrong!");
print("5. This NEVER executes"); // Unreachable!
}
// Output:
// 1. Main starts
// 2. Entering try block
// 3. Level one starts
// 4. Level two starts
// 8. Caught in main: Exception: Something went wrong!
// 9. Finally in main always runs
// 10. Main continuesဒီမှာကြည့်မယ်ဆို function အဆင့်ဆင့် ခေါ်သွားပြီး exception ကြုံတဲ့အခါ catch ထဲကဟာကို ခေါ်ပြီးတော့ နောက်ဆုံးတော့ finally ထဲမှာ ရေးထားတာကို ခေါ်သွားမှာပါ။ တကယ်လို့ exception မရှိဘူးဆိုရင်တော့ catch ထဲမှာ ပေးထားတာကို ခေါ်မှာမဟုတ်ဘဲ finally ထဲမှာ ရေးထားတာကို တန်းခေါ်သွားမှာပဲ ဖြစ်ပါတယ်။
Stack Traces
Program တွေ run တဲ့အချိန် function တွေ အဆင့်ဆင့် ခေါ်သွားတာမျိုးမှာ ဘယ်အဆင့် ဘယ်နေရာမှာ exception ဖြစ်သွားတာလဲဆိုတဲ့ information တွေကိုတော့ strack trace ထဲကနေ ရယူနိုင်ပါတယ်။ function တွေ အဆင့်ဆင့်ခေါ်တာက stack data structure နဲ့ အလုပ်လုပ်ပါတယ်။ ဒါကြောင့်မို့လို့ stack ကို trace လိုက်တာလို့ ခေါ်တာပါ။
void main() {
try {
performOperation();
} catch (e, stackTrace) {
print("ERROR: $e");
print("\n=== STACK TRACE ===");
print(stackTrace);
print("===================\n");
// Parse stack trace
final lines = stackTrace.toString().split('\n');
print("Error occurred in: ${lines.first}");
}
}
void performOperation() {
validateData();
}
void validateData() {
checkFormat();
}
void checkFormat() {
// This is where it fails
throw FormatException("Invalid data format");
}
Log ထဲမှာ stack trace ကို ကြည့်ခြင်းအားဖြင့် ဘယ် file ရဲ့ ဘယ်နေရာမှာ exception ဖြစ်နေလဲဆိုတာ အလွယ်တကူ တွေ့ရမှာပဲ ဖြစ်ပါတယ်။
Custom Exceptions
ဒီတော့ exception ကို သုံးခြင်းအားဖြင့် ဖြစ်နိုင်တဲ့ အဆင်မပြေဖြစ်တာတွေကို catch လုပ်ပြီး ဖြေရှင်းလို့ရတယ်ဆိုတာကို လေ့လာပြီးပြီပဲ ဖြစ်ပါတယ်။ ရိုးရိုး base exception ကိုမှ ထပ်ပြီးတော့ လိုချင်တဲ့ exception type မျိုးတွေအဖြစ်လဲ extends လုပ်လို့ရပါသေးတယ်။
class BasicException implements Exception {
final String message;
BasicException(this.message);
@override
String toString() => message;
}
// Professional custom exception with metadata
class NetworkException implements Exception {
final String message;
final int? statusCode;
final String? url;
final DateTime timestamp;
final Map<String, dynamic>? details;
NetworkException(this.message, {this.statusCode, this.url, this.details})
: timestamp = DateTime.now();
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('NetworkException: $message');
if (statusCode != null) buffer.writeln(' Status Code: $statusCode');
if (url != null) buffer.writeln(' URL: $url');
buffer.writeln(' Timestamp: $timestamp');
if (details != null) buffer.writeln(' Details: $details');
return buffer.toString();
}
// Helper method for user-friendly messages
String getUserMessage() {
return switch (statusCode) {
401 => "Please log in again",
404 => "Requested resource not found",
500 => "Server error. Please try again later",
503 => "Service temporarily unavailable",
_ => message,
};
}
}
// Usage
void main() {
try {
throw NetworkException(
"Failed to fetch user data",
statusCode: 404,
url: "https://api.example.com/users/123",
details: {'retry_after': 60, 'reason': 'User not found'},
);
} on NetworkException catch (e) {
print(e); // Detailed for logging
print("\nUser sees: ${e.getUserMessage()}"); // Friendly for UI
}
}
// Output:
// NetworkException: Failed to fetch user data
// Status Code: 404
// URL: https://api.example.com/users/123
// Timestamp: 2026-01-14 22:30:14.304483
// Details: {retry_after: 60, reason: User not found}
// User sees: Requested resource not foundဒီမှာ exception ကို catch ထဲမှာ တွေ့ရတဲ့အခါ information အပြည့်အစုံနဲ့ ဘာကြောင့် ဘယ်နေရာမှာ ဖြစ်လဲဆိုတာတွေပါ ပါဝင်လာတာကို တွေ့ရမှာပါ။ ဒီလိုမျိုး context ပိုများများနဲ့ လိုချင်တဲ့အခါ custom exception တွေကို အသုံးပြုလို့ ရတာပဲ ဖြစ်ပါတယ်။
နောက်ထပ် ဥပမာ တခုလောက် ထပ်ကြည့်ရအောင်ပါ။
abstract class AppException implements Exception {
final String message;
final DateTime timestamp;
AppException(this.message) : timestamp = DateTime.now();
@override
String toString() => "$runtimeType: $message";
}
// Authentication exceptions
class AuthException extends AppException {
AuthException(super.message);
}
class InvalidCredentialsException extends AuthException {
InvalidCredentialsException() : super("Email or password is incorrect");
}
class AccountLockedException extends AuthException {
final Duration lockDuration;
AccountLockedException(this.lockDuration)
: super("Account locked for ${lockDuration.inMinutes} minutes");
}
class SessionExpiredException extends AuthException {
SessionExpiredException() : super("Your session has expired");
}
// Data validation exceptions
class ValidationException extends AppException {
final Map<String, String> fieldErrors;
ValidationException(super.message, this.fieldErrors);
}
// Usage with granular handling
void handleLogin(String email, String password) {
try {
authenticateUser(email, password);
} on InvalidCredentialsException catch (e) {
// Show "wrong password" UI
print("Show error: ${e.message}");
print("UI: Shake the login form");
} on AccountLockedException catch (e) {
// Show lockout timer
print("Show lockout: ${e.message}");
print("UI: Display countdown timer");
} on SessionExpiredException catch (e) {
// Redirect to login
print("Redirect to: /login");
} on ValidationException catch (e) {
// Show field-specific errors
e.fieldErrors.forEach((field, error) {
print("$field error: $error");
});
} on AuthException catch (e) {
// Catch-all for any auth error
print("General auth error: ${e.message}");
} catch (e) {
// Unexpected error
print("Unexpected error: $e");
}
}
void authenticateUser(String email, String password) {
// Simulate different failure scenarios
if (password.isEmpty) {
throw ValidationException("Validation failed", {
'password': 'Password is required',
});
}
if (password.length < 6) {
throw InvalidCredentialsException();
}
// Success case
print("Login successful!");
}
void main() {
handleLogin('aung@aung.com', 'password');
}Result Pattern
ရှေ့ရက်တွေတုန်းက sealed class တွေ အကြောင်းပြောတုန်းက resutl pattern ဆိုတာကို ထည့်ပြောခဲ့တာ မှတ်မိမယ်ထင်ပါတယ်။ တကယ်လို့ ပြန်ဖတ်ချင်ရင် ဒီမှာ ဖတ်လို့ရပါတယ်။

အဲ့တုန်းက ပြခဲ့သလို အဆင်ပြေတာ (Success) နဲ့ မပြေတာ (Failure) ကို ခွဲရေးချင်တဲ့အခါ exception တွေအစား result pattern ကို အသုံးပြုလို့ရပါတယ်။ သူ့ကိုတော့ app layer boundary တွေ အထူးသဖြင့် UI layer boundary မှာ ပိုအသုံးများပါတယ်။ သူ့ဆီက ရလာတဲ့ result ပေါ်မူတည်ပြီး UI မှာ ပြချင်တာတွေ handle လုပ်ရတာ exception ထက် ပိုပြီး အဆင်ပြေလို့ ဖြစ်ပါတယ်။
sealed class Result<T> {
const Result();
bool get isSuccess => this is Success<T>;
bool get isFailure => this is Failure<T>;
}
class Success<T> extends Result<T> {
final T data;
const Success(this.data);
@override
String toString() => 'Success($data)';
}
class Failure<T> extends Result<T> {
final String message;
const Failure(this.message);
@override
String toString() => 'Failure($message)';
}အခြား service တွေက exception အနေနဲ့ throw ခဲ့ရင်တော့ result pattern ရဲ့ failure ကို ပြောင်းလဲပေးလိုက်တဲ့ခါ handle လုပ်ရတာ လွယ်ကူသွားပါတယ်။ Result pattern မှာတော့ Success ရော၊ Failure ရောကိုပါ data တွေအနေနဲ့ သတ်မှတ်ပြီး သက်ဆိုင်ရာ နေရာတွေကို ပို့ပေးတာမျိုးပဲ ဖြစ်ပါတယ်။ ရှေ့ရက်တုန်းက ပြောပြီးသားဆိုတော့ အရမ်းကြီး အသေးစိတ် မရှင်းတော့ပါဘူး။
ဒီနေ့ အဆုံးမသတ်ခင် safety net ဆိုတာလေးလဲ ထည့်ပြောချင်ပါတယ်။ သူ့ကတော့ အပေါ်မှာ handle လုပ်ရမယ့်နေရာတွေက handle မလုပ်ဘဲ exception/error တခုက app ကို crush တော့မယ်ဆိုရင် အဲ့ဒီလိုမဖြစ်အောင် ကာကွယ်ပေးမယ့်ဟာပဲ ဖြစ်ပါတယ်။ ဒါကိုတော့ runZonedGuarded လို့ခေါ်ပါတယ်။
void main() async {
runZonedGuarded(
() async {
...
},
(error, stackTrace) {
print("🚨 ZONE CAUGHT ERROR:");
print(" Error: $error");
print(
" Stack: ${stackTrace.toString().split('\n').take(3).join('\n')}",
);
},
);
}ဒီလိုမျိုး ရေးလိုက်ခြင်းအားဖြင့် ဘယ်နေရာမှ handle လုပ်မထား error/exception တွေအကုန်လုံး runZonedGuarded ထဲ ရောက်လာမှာပဲ ဖြစ်ပါတယ်။
ဒီနေ့ Day 14 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။
- Youtube: https://www.youtube.com/@arkarmintun
- Newsletter: https://arkar.dev/
- Discord: https://discord.gg/3xUJ6k6dkH
