例外錯誤處理

Dart 所有例外皆為非檢查型例外,函數不需宣告所拋出的錯誤類型,也不會強制開發者進行捕捉

分為兩大類:

  • Exception:業務邏輯可預期錯誤(格式錯誤、參數異常等,建議捕捉並處理)
  • Error:程式致命錯誤(型別錯誤、空指標、未實作方法等,通常不建議捕捉)

任何非 null 的物件都能透過 throw 拋出,正式專案規範僅允許拋出 Exception/Error 的子類別

未被捕捉的例外會直接終止當前隔離執行緒(isolate),造成程式崩潰

throw 拋出例外

1. 拋出系統內建例外

void main() {
  // 格式例外
  throw FormatException("至少需要1段資料");
}
Code language: Dart (dart)
D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
Unhandled exception:
FormatException: 錯誤描述
#0      main (file:///D:/dartdemo/firstdart/bin/firstdart.dart:3:3)
#1      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#2      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)Code language: PHP (php)

上面這段程式會手動拋出自訂描述的系統例外,執行後就會印出錯誤。你可能會覺得這段程式沒什麼實用價值,但拋出例外是開發上非常重要的機制。舉例使用者註冊場景:我們要驗證使用者輸入格式是否合法,一旦格式不符就直接拋出例外提示使用者,同時中斷流程,避免不合法的髒資料流入,維持資料完整性。

2. 拋出任意物件

正式上線專案不推薦此寫法

void main() {
  throw "文字描述!";
}Code language: Dart (dart)

執行結果

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
Unhandled exception:
文字描述!
#0      main (file:///D:/dartdemo/firstdart/bin/firstdart.dart:2:3)
#1      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#2      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)Code language: PHP (php)

3. 箭頭函式內使用 throw(throw 屬於運算式)

class Point {}
void distanceTo(Point other) => throw UnimplementedError("該方法尚未實作");

void main() {
  distanceTo(Point());
}
Code language: JavaScript (javascript)

三、try /on/catch 捕捉例外

語法差異說明

  • on 例外類型:只匹配指定類型的例外,無法取得例外物件實體
  • catch(e):捕捉所有類型例外,變數 e 存放例外實例
  • catch(e, s):e = 例外物件,s = 呼叫堆疊資訊 StackTrace

1:使用 on 單一指定類型捕捉


//自訂例外類別,實作 Exception
class OutOfLlamasException implements Exception {}

//拋出自訂例外的函式
void breedMoreLlamas() {
  throw OutOfLlamasException();
}

void buyMoreLlamas() {
  print("執行了buyMoreLlamas");
}

void main() {
  try {
    breedMoreLlamas(); //呼叫執行有風險的程式
  } on OutOfLlamasException {
    buyMoreLlamas(); //若捕捉到 OutOfLlamasException 就執行這段邏輯
  }
}Code language: Dart (dart)

執行結果

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
執行了buyMoreLlamasCode language: CSS (css)
try  {
    //預期會出錯的程式
} on 例外類別 {
   //捕捉到錯誤後要執行的邏輯
}Code language: Dart (dart)

try on 就是專門用來依類型捕捉例外的語法。

2:多層分支分級捕捉

精準匹配優先執行

class OutOfLlamasException implements Exception {}

void breedMoreLlamas() {
  throw FormatException("資料格式錯誤");
}

void main() {
  try {
    breedMoreLlamas();
  }
  // 1. 精準匹配自訂例外
  on OutOfLlamasException {
    print("OutOfLlamasException 發生錯誤");
  }
  // 2. 匹配所有 Exception 子類別
  on Exception catch (e) {
    print("一般業務例外:$e");
  }
  // 3. 兜底捕捉 Error 與其他任意拋出物件
  catch (e) {
    print("未知嚴重錯誤:$e");
  }
}
Code language: Dart (dart)

輸出 : 一般業務例外:FormatException: 資料格式錯誤

我們可以透過多個 on 區塊分開捕捉特定 Exception,最後搭配 catch 做全域兜底。

3:捕捉例外並印出完整呼叫堆疊 StackTrace

void main() {
  try {
    int a = null;
    print(a + 1);
  } catch (e, stack) {
    print("例外資訊:$e");
    print("完整呼叫堆疊:\n$stack");
  }
}
Code language: Dart (dart)

這段程式除了印出錯誤提示,還會輸出完整的函數呼叫鏈,方便除錯定位問題。

D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:3:13: Error: A value of type 'Null' can't be assigned to a variable of type 'int'.
    int a = null;Code language: PHP (php)

四、rethrow 重新拋出

局部處理部分邏輯後,將例外繼續向上層傳遞

用途:當前函式先處理一部分復原邏輯,再把例外交給上一層呼叫者繼續處理

有時我們不能只在當前函式處理完錯誤,還需要把例外往上拋,讓呼叫此函式的上層程式也同步處理。

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // true 無法執行遞增,執行期觸發錯誤
  } catch (e) {
    print("misbehave 局部處理:${e.runtimeType}");
    rethrow; // 重新拋出,交給外層 try 捕捉
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print("main 最終處理:${e.runtimeType}");
  }
}
Code language: Dart (dart)

執行流程:

misbehave 局部處理:NoSuchMethodError
main 最終處理:NoSuchMethodError

範例中的例外會同時在 misbehave 與 main 兩個區塊分別處理。

五、finally 最終區塊

不論程式是否發生錯誤,一定會執行

兩種常見使用場景:

  1. 僅搭配 try + finally:例外不會被捕捉,執行完 finally 邏輯後程式直接崩潰
  2. try + catch + finally:先處理例外,最後執行 finally(專門用於釋放資源)

1:try + finally

void cleanLlamaStalls() => print("清理佔用資源,無論如何必定執行");
void breedMoreLlamas() => throw Exception("程式發生錯誤");

void main() {
  try {
    breedMoreLlamas();
  } finally {
    cleanLlamaStalls();
  }
}
Code language: Dart (dart)

執行結果:雖然 finally 的清理程式會執行,但未被捕捉的例外依舊會造成程式崩潰。

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
清理佔用資源,無論如何必定執行
Unhandled exception:
Exception: 程式發生錯誤
#0      breedMoreLlamas (file:///D:/dartdemo/firstdart/bin/firstdart.dart:2:27)
#1      main (file:///D:/dartdemo/firstdart/bin/firstdart.dart:6:5)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#3      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)Code language: PHP (php)

2:try + catch + finally

釋放資源的標準寫法,例如開啟檔案後必須關閉釋放

void cleanLlamaStalls() => print("關閉檔案、釋放系統資源");
void breedMoreLlamas() => throw FormatException("資料解析失敗");

void main() {
  try {
    breedMoreLlamas();
  } catch (e) {
    print("捕捉到例外:$e");
  } finally {
    cleanLlamaStalls();
  }
}
Code language: Dart (dart)

執行順序:觸發例外 → catch 處理錯誤 → 執行 finally

執行結果

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
捕捉到例外:FormatException: 資料解析失敗
關閉檔案、釋放系統資源
Code language: CSS (css)

六、assert 斷言

僅開發除錯階段生效,正式發布版本會直接失效

assert(布林判斷條件, [可選自訂錯誤提示文字])

當判斷條件為 false 時,會拋出 AssertionError;正式編譯後會完全忽略斷言,不會產生任何效能損耗。

1:基礎斷言範例

void main() {
  String? text;
  int number = 120;
  String url = "http://foxdevelop.com";

  assert(text != null); // 空值檢查
  assert(number < 100); // 數值範圍驗證
  assert(url.startsWith("https")); // 網址協定檢查
}
Code language: JavaScript (javascript)

直接執行這段程式,畫面上不會輸出任何內容!

原因是斷言預設為關閉狀態,必須手動帶參數開啟才會生效,繼續往下閱讀開啟方式。

2:自訂斷言失敗提示文字

void main() {
  String url = "http://test.com";
  assert(
    url.startsWith("https"),
    "連結 $url 必須以 https 開頭",
  );
}
Code language: JavaScript (javascript)

我們可以自行定義斷言失敗時顯示的錯誤訊息。

斷言開啟方式

  1. Flutter Debug 偵錯模式預設自動開啟斷言
  2. dart 指令執行:dart --enable-asserts 你的dart檔名.dart
  3. 正式打包編譯:所有斷言程式碼會被移除,無額外效能耗損
D:\dartdemo\firstdart\bin>dart --enable-asserts firstdart.dart
Unhandled exception:
'file:///D:/dartdemo/firstdart/bin/firstdart.dart': Failed assertion: line 6 pos 10: 'text != null': is not true.
#0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:67:4)
#1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:49:5)
#2      main (file:///D:/dartdemo/firstdart/bin/firstdart.dart:6:10)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#4      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)Code language: PHP (php)

dart run 與單純 dart 指令預設都會關閉斷言,必須追加 --enable-asserts 參數:

執行指令範例

dart run --enable-asserts main.dartCode language: CSS (css)

若終端目前位於專案根目錄,可直接簡寫:

執行指令:

dart run --enable-asserts

dart run:直接執行 Dart 原始碼,適合開發階段測試

dart 檔名:簡化單檔執行語法

dart compile exe:編譯獨立執行檔,編譯成品永遠停用 assert

Dart 程式存放於專案內的 bin 資料夾,你可以切進 bin 資料夾執行單一檔案,或是切回上一層專案根目錄執行整個專案。

dart run –enable-asserts

下方示範兩種執行方式:直接在 bin 內執行,與回到專案根目錄執行。

D:\dartdemo\firstdart\bin>dart --enable-asserts firstdart.dart
Unhandled exception:
'file:///D:/dartdemo/firstdart/bin/firstdart.dart': Failed assertion: line 6 pos 10: 'text != null': is not true.
#0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:67:4)
#1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:49:5)
#2      main (file:///D:/dartdemo/firstdart/bin/firstdart.dart:6:10)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#4      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)

D:\dartdemo\firstdart\bin>dart run
Building package executable...
Built firstdart:firstdart.

D:\dartdemo\firstdart\bin>cd ..

D:\dartdemo\firstdart>dart run --enable-asserts
Building package executable...
Built firstdart:firstdart.
Unhandled exception:
'file:///D:/dartdemo/firstdart/bin/firstdart.dart': Failed assertion: line 6 pos 10: 'text != null': is not true.
#0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:67:4)
#1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:49:5)
#2      main (file:///D:/dartdemo/firstdart/bin/firstdart.dart:6:10)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#4      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)Code language: PHP (php)

若斷言判斷條件為 true,則不會拋出任何錯誤

修改範例程式:將網址的 http 改為 https、text != null 改成 text == null、number 數值改為 99

void main() {
  String? text;
  int number = 99;
  String url = "https://foxdevelop.com";

  assert(text == null); // 空值檢查
  assert(number < 100); // 數值範圍驗證
  assert(url.startsWith("https")); // 網址協定檢查
}Code language: Dart (dart)

再次執行就不會觸發任何例外

D:\dartdemo\firstdart>dart run --enable-asserts
Building package executable...
Built firstdart:firstdart.Code language: CSS (css)

七、綜合完整範例

void cleanResource() => print("finally:釋放佔用資源");

void testError() {
  try {
    int num = 150;
    assert(num < 100, "數字必須小於100");
    throw FormatException("資料格式非法");
  } on FormatException catch (e) {
    print("捕捉格式例外:$e");
    rethrow;
  } finally {
    cleanResource();
  }
}

void main() {
  try {
    testError();
  } catch (e) {
    print("main 捕捉二次拋出的例外:$e");
  }
}
Code language: PHP (php)

執行結果

D:\dartdemo\firstdart>dart run --enable-asserts
Building package executable...
Built firstdart:firstdart.
finally:釋放佔用資源
main 捕捉二次拋出的例外:'file:///D:/dartdemo/firstdart/bin/firstdart.dart':
 Failed assertion: line 6 pos 12: 'num < 100': 數字必須小於100Code language: PHP (php)

完整例外錯誤處理示範

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *