昨日、Coroutineを理解するために以下の記事を書いた
Kotlin Coroutinesパターン&アンチパターン - Qiita
https://qiita.com/ikemura23/items/fb8caeba4c35fcd85644
ただ書いただけではなく、本当にそうなのか実装して確かめてみた。
例外処理をtry/catchに頼らない
エラーが発生するdoWork()
をtry / catchしてもダメ
class MainActivity : AppCompatActivity() {
private val job = Job()
private val scope = CoroutineScope(Dispatchers.Default + job)
override fun onResume() {
super.onResume()
// Throw Exceptionの可能性がある処理
fun doWork(): Deferred<String> = scope.async { throw UnknownError() }
scope.launch {
try {
doWork().await() // ここで落ちる
} catch (e: Exception) {
Log.d(TAG, "ここに到達しない")
}
}
}
結果
実行すると、trt / catchの意味なくエラーでアプリが落ちる
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.ikemura.android_kotlin_lab, PID: 9287
java.util.concurrent.TimeoutException
...
解決策
SupervisorJob
で子のエラーを回避する。
class MainActivity : AppCompatActivity() {
private val job = SupervisorJob() // <= SupervisorJobに変更
private val scope = CoroutineScope(Dispatchers.Default + job)
override fun onResume() {
super.onResume()
// Throw Exceptionの可能性がある処理
fun doWork(): Deferred<String> = scope.async { throw UnknownError() }
scope.launch {
try {
doWork().await() // ここで落ちる
} catch (e: Exception) {
Log.d(TAG, "キャッチされる")
}
}
}
注意すべきasync
↑で示した解決は、SupervisorJobを使用してCoroutineScopeで非同期を明示的に実行した場合にのみ機能する。
以下の async はSupervisorJob
を使用してもエラーとなる。
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
// エラーの可能性がある処理
fun doWork(): Deferred<String> = throw TimeoutException()
fun loadData() = scope.launch {
try {
async {
doWork() // ここで落ちる
}
} catch (e: Exception) {
Log.d(TAG, "キャッチされない")
}
}
理由は、async内で別のcoroutineが作成され、その範囲内で起動されるから。(ちょっと曖昧)
解決策
coroutineScopeを使ってasyncをラップする。
// エラーの可能性がある処理
fun doWork(): Deferred<String> = throw TimeoutException()
fun loadData() {
scope.launch {
try {
coroutineScope { // 追加
async {
doWork()
}
}
Log.d(TAG, "ここは実行されない")
} catch (e: Exception) {
Log.d(TAG, "キャッチされる")
}
}
}
上記コードは分かりにくいので、こう見やすくできる。
// エラーの可能性がある処理
suspend fun doWork(): Deferred<String> = coroutineScope {
async { throw TimeoutException() }
}
fun loadData() {
scope.launch {
try {
doWork() // coroutineScopeとasyncをdoWorkに移動させた
Log.d(TAG, "ここは実行されない")
} catch (e: Exception) {
Log.d(TAG, "キャッチされる")
}
}
}
try/catchばかり使わない
SupervisorJobを作り、try/catchでラップしておけば大概のエラーはキャッチできるはず。
だが毎回try/catchを使うのではなく、コルーチンの例外をキャッチしてくれるCoroutineExceptionHandlerが用意されているので、こちらを使う。
CoroutineExceptionHandlerを個別に指定する例
// エラーハンドリングを作成
private val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e(TAG, "例外キャッチ $throwable") // ログ出力のみ
}
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
// エラーの可能性がある処理
fun doWork(): Deferred<String> = throw TimeoutException()
fun loadData() {
scope.launch(exceptionHandler) { // エラーハンドリングを指定
doWork()
}
}
ログ
E/MainActivity: 例外キャッチ java.util.concurrent.TimeoutException
try/catchがなくなり、スッキリした。
CoroutineExceptionHandlerを共通で使い回す
CoroutineExceptionHandlerを個別に指定するのもいいが、CoroutineScopeに渡せば共通で使い回せる。
private val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e(TAG, "例外キャッチ $throwable")
}
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job + exceptionHandler) // exceptionHandlerを渡す
// エラーの可能性がある処理
fun doWork(): Deferred<String> = throw TimeoutException()
fun loadData() {
scope.launch { // exceptionHandler指定が無くなった
doWork()
Log.d(TAG, "到達しない")
}
}
普段は共通でしておき、必要な時だけ個別に例外を指定するのがベストだろう。
実装したコード
参考リンク