【Dart】ExceptionとErrorを分けて考える

Zennの記事と同じ内容のものになります。

https://zenn.dev/1mash0/articles/6e2199abebe1ff


# Dartにおける`Exception`と`Error`の扱い
Dartではエラーを大別すると`Exception`と`Error`の2種類に分けられるのですが、エラー発生時の扱いが異なります。

`Exception`は catch されることを想定しており、プログラムで対応できるエラーを扱います。

逆に`Error`は catch されることを想定しておらず、プログラムの実装が間違っているため発生するエラーを扱います。
実装者が避けるべきものであり、もし`Error`が発生した場合catchするのではなくプログラムを終了させるようドキュメントに書かれています。

catch されることを意図していないエラーについては`Error`やそのサブクラスを用いるよう`Exception`のドキュメントにも書かれています。

## `Exception`

https://api.flutter.dev/flutter/dart-core/Exception-class.html

> (原文)
> A marker interface implemented by all core library exceptions.
>
> An Exception is intended to convey information to the user about a failure, so that the error can be addressed programmatically. It is intended to be caught, and it should contain useful data fields.
> 
> Creating instances of Exception directly with Exception("message") is discouraged in library code since it doesn't give users a precise type they can catch. It may be reasonable to use instances of this class in tests or during development.
>
> For failures that are not intended to be caught, use Error and its subclasses.
>
> *(ChatGPT 和訳)*
> *全てのコアライブラリ例外によって実装されるマーカーインターフェース。*
>
> *Exception（例外）は、エラーが発生した際に、そのエラーをプログラムで対応できるようにユーザーに情報を伝えることを目的としています。この例外は捕捉されることを意図しており、有用なデータフィールドを含むべきです。*
>
> *ライブラリコード内で直接 Exception("message") のように Exception のインスタンスを作成することは推奨されていません。なぜなら、ユーザーが捕捉できる正確な型を提供しないからです。しかし、テストや開発中にこのクラスのインスタンスを使用することは合理的です。*
>
> *捕捉されることを意図していない失敗については、Error やそのサブクラスを使用してください。*

## `Error`

https://api.flutter.dev/flutter/dart-core/Error-class.html

> (原文)
> Error objects thrown in the case of a program failure.
>
> An Error object represents a program failure that the programmer should have avoided.
> 
> Examples include calling a function with invalid arguments, or even with the wrong number of arguments, or calling it at a time when it is not allowed.
> 
> These are not errors that a caller should expect or catch — if they occur, the program is erroneous, and terminating the program may be the safest response.
> 
> When deciding that a function should throw an error, the conditions where it happens should be clearly described, and they should be detectable and predictable, so the programmer using the function can avoid triggering the error.
>
> *(ChatGPT 和訳)*
> *プログラムの失敗時にスローされるErrorオブジェクト。*
>
> *Errorオブジェクトは、プログラマーが回避すべきプログラムの失敗を表します。*
>
> *例えば、無効な引数を使って関数を呼び出した場合や、誤った引数の数で関数を呼び出した場合、または許可されていないタイミングで呼び出した場合などが挙げられます。*
>
> *これらは呼び出し側が期待したり捕捉したりすべきエラーではありません。もしこれらが発生した場合、プログラムは誤っており、プログラムを終了させることが最も安全な対応になる可能性があります。*
>
> *関数がエラーをスローするように設計する際には、その条件を明確に記述し、エラーが発生する状況が検出可能で予測可能であるべきです。これにより、関数を使用するプログラマーがそのエラーを引き起こさないように回避できるようになります。*

# エラーハンドリング
このことを基に、エラーハンドリングのベストプラクティスは Effective Dart の Error Handling の項に記載されています。

https://dart.dev/effective-dart/usage#error-handling

## AVOID catches without `on` clauses
`on`句を用いて catch する`Exception`や`Error`の種別をフィルタリングするべき。
`on`句を用いないとあらゆる例外(`Exception`も`Error`も)を catch してしまい`ArgumentError`なども握りつぶしてしまう。(→[Pokémon exception handling](https://blog.codinghorror.com/new-programming-jargon/))

もし実行時エラーをハンドリングしたい場合は、基底クラスである`Exception`を catch すると良い

```dart
try {
  ...
} on FormatException catch (e) {
  // FormatExceptionのハンドリング
} on Exception (e) {
  // その他例外のハンドリング
}
```

> <details><summary>原文</summary>
>
> A catch clause with no on qualifier catches anything thrown by the code in the try block. Pokémon exception handling is very likely not what you want. Does your code correctly handle StackOverflowError or OutOfMemoryError? If you incorrectly pass the wrong argument to a method in that try block do you want to have your debugger point you to the mistake or would you rather that helpful ArgumentError get swallowed? Do you want any `assert()` statements inside that code to effectively vanish since you're catching the thrown AssertionErrors?
> 
> The answer is probably "no", in which case you should filter the types you catch. In most cases, you should have an `on` clause that limits you to the kinds of runtime failures you are aware of and are correctly handling.
>
> In rare cases, you may wish to catch any runtime error. This is usually in framework or low-level code that tries to insulate arbitrary application code from causing problems. Even here, it is usually better to catch Exception than to catch all types. Exception is the base class for all runtime errors and excludes errors that indicate programmatic bugs in the code.
</details>

## DON'T discard errors from catches without `on` clauses
もし`on`句を用いず全てのエラーを catch する場合は、ログへの記録、ユーザーへの表示、必要に応じて再スローするなどして握り潰さないようにする。

```dart
try {
  ...
} catch (e) {
  // ログとして保存
  print(e);

  // 必要に応じて再スロー
  rethrow;
}
```

> <details><summary>原文</summary>
> 
> If you really do feel you need to catch everything that can be thrown from a region of code, do something with what you catch. Log it, display it to the user or rethrow it, but do not silently discard it.
</details>


## DO throw objects that implement `Error` only for programmatic errors
`Error`もしくはそのサブクラスがスローされた場合、それはコードにバグが存在することを意味する。
また、APIが誤って使用されているなどコード修正が必要な場合に`Error`をスローする。

それ以外の実行時エラーの場合は **`Error`をスローするのではなく**、`Exception`や他の型をスローするようにする。

> <details><summary>原文</summary>
>
> The Error class is the base class for programmatic errors. When an object of that type or one of its subinterfaces like ArgumentError is thrown, it means there is a bug in your code. When your API wants to report to a caller that it is being used incorrectly throwing an Error sends that signal clearly.
>
> Conversely, if the exception is some kind of runtime failure that doesn't indicate a bug in the code, then throwing an Error is misleading. Instead, throw one of the core Exception classes or some other type.
</details>


## DON'T explicitly catch `Error` or types that implement it
`Error`を catch してしまうとバグが隠れてしまう。
発生した`Error`のハンドリング処理を追加するのではなく、プログラムを停止させ原因となるコードの修正をするべき。

> <details><summary>原文</summary>
>
> This follows from the above. Since an Error indicates a bug in your code, it should unwind the entire callstack, halt the program, and print a stack trace so you can locate and fix the bug.
>
> Catching errors of these types breaks that process and masks the bug. Instead of adding error-handling code to deal with this exception after the fact, go back and fix the code that is causing it to be thrown in the first place.
</details>


## DO use `rethrow` to rethrow a caught exception
`Exception`を再スローする場合は、例外オブジェクトを`throw`で投げ直すのではなく、`rethrow`を使用する。
`throw`で投げ直すとスタックトレースが最後にスローされた位置にリセットされてしまうが、`rethrow`なら元のスタックトレースが保持される。
```dart
try {
  somethingRiskey();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}
```

> <details><summary>原文</summary>
> 
> If you decide to `rethrow` an exception, prefer using the rethrow statement instead of throwing the same exception object using `throw`. `rethrow` preserves the original stack trace of the exception. `throw` on the other hand resets the stack trace to the last thrown position.
</details>

# まとめ
Dartでは`Error`とそのサブクラスは、「実装ミスで、コードの修正が必要」であるため catch しない方が良いということがわかりました。
`on`句を用いないと`Exception`だけでなく`Error`も一緒に catch してハンドリングされてしまうため、`Exception`の種別ごとに`on`句でフィルタリングをし、それ以外の実行時エラーをハンドリングする場合は`Exception`を指定するようにして、`Error`がハンドリングされないよう注意する必要があります。

また独自のエラークラスを実装する場合は、スローされる独自実装のエラーが「コードのバグで修正が必要なものなのか」という点を考えて、`Exception`を継承するのか`Error`を継承するのか考えるべきだなと思います。
それに伴う命名も注意が必要だと思います。

ハンドリングされる想定のエラーなのに「`**Error`」という名前は不適切だと思いますし、反対に「`**Exception`」というエラーがスローされてアプリがクラッシュしてると実装ミスなのか、命名が間違ってるだけなのかの判断がつかないです。

# 参考サイト
https://api.flutter.dev/flutter/dart-core/Exception-class.html
https://api.flutter.dev/flutter/dart-core/Error-class.html
https://dart.dev/effective-dart/usage#error-handling