Open haikalpribadi opened 2 years ago
In 1.1.0-alpha03 you can override it:
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.LocalWindowExceptionHandlerFactory
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowExceptionHandler
import androidx.compose.ui.window.WindowExceptionHandlerFactory
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import androidx.compose.ui.window.singleWindowApplication
import java.awt.Window
import java.awt.event.WindowEvent
import kotlin.system.exitProcess
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
var lastError: Throwable? by mutableStateOf(null)
application(exitProcessOnExit = false) {
CompositionLocalProvider(
LocalWindowExceptionHandlerFactory provides object : WindowExceptionHandlerFactory {
override fun exceptionHandler(window: Window) = WindowExceptionHandler {
lastError = it
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
throw it
}
}
) {
Window(onCloseRequest = ::exitApplication) {
throw RuntimeException()
}
}
}
if (lastError != null) {
singleWindowApplication(
state = WindowState(width = 200.dp, height = Dp.Unspecified),
exitProcessOnExit = false
) {
Text(lastError?.message ?: "Unknown error", Modifier.padding(8.dp))
}
exitProcess(1)
} else {
exitProcess(0)
}
}
But it doesn't catch errors in application
block. Catching errors in application
block in pre-1.0.0 versions was "accidental" feature of which I was not aware. It seems a good feature, we will look how it can be properly implemented.
I agree with @haikalpribadi about this. Current API with LocalWindowExceptionHandlerFactory is very bulky and old API with root-catching would be more convenient to use.
P.S. Also, @igordmn your snippet doesn't work if exceptionHandler throws error again after window closing call. The program doesn't get to the code with the error window and just terminates.
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
var lastError: Throwable? by mutableStateOf(null)
application(exitProcessOnExit = false) {
CompositionLocalProvider(
LocalWindowExceptionHandlerFactory provides object : WindowExceptionHandlerFactory {
override fun exceptionHandler(window: Window) = WindowExceptionHandler {
lastError = it
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
// throw it
}
}
) {
Window(onCloseRequest = ::exitApplication) {
throw RuntimeException()
}
}
}
if (lastError != null) {
singleWindowApplication(
state = WindowState(width = 200.dp, height = Dp.Unspecified),
exitProcessOnExit = false
) {
Text(lastError?.message ?: "Unknown error", Modifier.padding(8.dp))
}
exitProcess(1)
} else {
exitProcess(0)
}
}
I've been trying to get this to work for the better part of 3 hours now, and this is my progress.
Up in my main composable:
@Composable
fun MainUi() {
val appState = remember { AppState() }
AppTheme(appState.themeOption) {
BetterErrorHandling {
MainAppContent(appState)
}
}
}
BetterErrorHandling
is:
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun BetterErrorHandling(content: @Composable () -> Unit) {
val errorDialogState = remember { ErrorDialogState() }
CompositionLocalProvider(LocalWindowExceptionHandlerFactory provides WindowExceptionHandlerFactory {
WindowExceptionHandler { cause ->
errorDialogState.showError(error = cause, source = ErrorDialogState.Source.ACTION)
}
}) {
content()
}
BetterErrorDialog(errorDialogState)
}
BetterErrorDialog
is not actually a better error dialog but just a placeholder:
@Composable
internal fun BetterErrorDialog(state: ErrorDialogState) {
if (state.isVisible) {
Dialog(onDismissRequest = { state.dismissError() }) {
Text(text = "*** YOU HAVE AN ERROR ***")
}
}
}
Despite replacing LocalWindowExceptionHandlerFactory
, the replacement doesn't get used. I can breakpoint in DefaultWindowExceptionHandlerFactory
and still see it getting all the attention even though I'm trying to bypass it.
Trying again on v1.6.1 - the behaviour is slightly different to before. My exception handler is still not called, but now the default one doesn't seem to be called either.
I have three unit tests where a synthetic exception is thrown from three different possible places:
onClick
callbackonClick
callbackAll three tests seem to fail in the same way - the exception becomes the test failure. This in itself is a little interesting, because it implies the exception thrown from inside the onClick
handler did get handled somewhere, but it was handled by propagating the exception into my test code, rather than by the handler I was trying to assign!
I too cannot get LocalWindowExceptionHandlerFactory
to work. It is simply using DefaultWindowExceptionHandlerFactory
and ignoring my provided replacement.
Trying again on v1.6.1 - the behaviour is slightly different to before. My exception handler is still not called, but now the default one doesn't seem to be called either.
I have three unit tests where a synthetic exception is thrown from three different possible places:
- Directly from the composition
- From a button's
onClick
callback- From inside a coroutine spawned from a button
onClick
callbackAll three tests seem to fail in the same way - the exception becomes the test failure. This in itself is a little interesting, because it implies the exception thrown from inside the
onClick
handler did get handled somewhere, but it was handled by propagating the exception into my test code, rather than by the handler I was trying to assign!
@hakanai hi, maybe you can try this, its worked for me: https://stackoverflow.com/questions/76061623/how-to-restart-looper-when-exception-throwed-in-jetpack-compose
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
Before the official 1.0.0 release, we had this exception handling logic at the root of our system (i.e.
main()
function), and it was working flawlessly and provided very useful information with stack-trace logging, etc.Whenever there was an unhandled error in
MainWindow()
application, we're able to provide thorough information inErrorWindow()
application, decorated as we see fit. However, since the 1.0 release, Compose Desktop framework now seems to override this and just throws its own exception in its own window -- undecorated and uninformative. It only shows the 1 line error message string, which is rarely enough information.How can we re-enable the old behaviour where we handle the exceptions ourselves at the root of the application? Is there a way to turn off the current automated error handling behaviour? I can't find any options to configure this in
androidx.compose.ui.window.application()
andandroidx.compose.ui.window.Window()
. Perhaps you can provide an option to turn it off through their method arguments?