SafeLaunch.kt
Each line, with the release that last changed it.
| 0.5.0 | 1 | package dev.stave.ui |
| 2 | ||
| 3 | import androidx.lifecycle.ViewModel | |
| 4 | import androidx.lifecycle.viewModelScope | |
| 5 | import kotlinx.coroutines.CoroutineExceptionHandler | |
| 6 | import kotlinx.coroutines.CoroutineScope | |
| 7 | import kotlinx.coroutines.Job | |
| 8 | import kotlinx.coroutines.launch | |
| 9 | ||
| 10 | /** | |
| 11 | * Coroutine exception handler shared by every ViewModel.launch site. | |
| 12 | * | |
| 13 | * Without this, uncaught exceptions from viewModelScope.launch bubble up to the JVM | |
| 14 | * default handler, which Compose Desktop / Swing surfaces as an opaque "Error" | |
| 15 | * dialog showing only the exception message — e.g. `index: 57, size: 57` after a | |
| 16 | * right-click → Play Next, with no stack trace and no way to recover gracefully. | |
| 17 | * | |
| 18 | * Logging here means the same exception becomes a stderr stack trace | |
| 19 | * (recoverable from `./gradlew :desktop:run` output or a shell-launched packaged | |
| 20 | * install) and the user keeps playing music. The app remains responsive. | |
| 21 | * | |
| 22 | * Note: This swallows the exception by *handling* it — viewModelScope's | |
| 23 | * SupervisorJob doesn't cancel siblings, so other launches continue normally. | |
| 24 | */ | |
| 25 | private val viewModelExceptionHandler = CoroutineExceptionHandler { _, throwable -> | |
| 26 | System.err.println( | |
| 27 | "[Earhorn] ViewModel coroutine threw (${throwable.javaClass.simpleName}):" | |
| 28 | ) | |
| 29 | throwable.printStackTrace(System.err) | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Like viewModelScope.launch, but installs [viewModelExceptionHandler] so an | |
| 34 | * unexpected throw becomes a logged stack trace rather than an error dialog. | |
| 35 | * | |
| 36 | * Use for any coroutine triggered by a user action (right-click handlers, | |
| 37 | * button clicks, menu items, drag callbacks, etc). Long-running collection | |
| 38 | * jobs (state flows, lifecycle observers) can also use this; the handler | |
| 39 | * only fires on actually-uncaught exceptions. | |
| 40 | */ | |
| 41 | fun ViewModel.safeLaunch(block: suspend CoroutineScope.() -> Unit): Job = | |
| 42 | viewModelScope.launch(viewModelExceptionHandler, block = block) |


