🤔

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

2024/09/06に公開

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

https://qiita.com/1mash0/items/54243db5fcb1a730e78d

DartにおけるExceptionErrorの扱い

Dartではエラーを大別するとExceptionErrorの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 するExceptionErrorの種別をフィルタリングするべき。
on句を用いないとあらゆる例外(ExceptionErrorも)を catch してしまいArgumentErrorなども握りつぶしてしまう。(→Pokémon exception handling)

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

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

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.

DON'T discard errors from catches without on clauses

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

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

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

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.

DO throw objects that implement Error only for programmatic errors

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

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

原文

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.

DON'T explicitly catch Error or types that implement it

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

原文

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.

DO use rethrow to rethrow a caught exception

Exceptionを再スローする場合は、例外オブジェクトをthrowで投げ直すのではなく、rethrowを使用する。
throwで投げ直すとスタックトレースが最後にスローされた位置にリセットされてしまうが、rethrowなら元のスタックトレースが保持される。

try {
  somethingRiskey();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}
原文

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.

まとめ

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

Discussion