compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

Is there a component similar to the ErrorBoundary in React in Compose?

Open ShirasawaSama opened this issue 2 years ago • 23 comments

I need to load third-party codes while building my UI, but the third-party code may throw exceptions during the composing. Then the whole program crashes.

So I'm wondering if there is an ErrorBoundary in Compose similar to the one in React to implement child component exception catching?

I tried the following code:

@Composable
fun ErrorBoundary(content: @Composable () -> Unit) {
    try {
        content()
    } catch (e: Throwable) {
        e.printStackTrace()
    }
}

But I got Try catch is not supported around composable function invocations.

Then I try to use the following very hacks code:

@Composable
@Suppress("ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
fun ErrorBoundary(content: @Composable () -> Unit) {
    val stack = currentComposer::class.java.getDeclaredField("pendingStack").apply { isAccessible = true }
    val curStack = stack.get(currentComposer)
    val backing = androidx.compose.runtime.Stack::class.java.getDeclaredField("backing").apply { isAccessible = true }
    val curBacking = ArrayList(backing.get(curStack) as ArrayList<*>)
    try {
        content()
    } catch (e: Throwable) {
        e.printStackTrace()
        backing.set(curStack, curBacking)
    }
}

@Composable
fun Test() {
    ErrorBoundary {
        Text("Test")
        throw RuntimeException("Test Error!")
        Text("Error")
    }
}

@Composable
fun App() { Test() }

However, if the exception occurs in a deeper component or there is a SideEffect, the above code will not work.

Because my program will load third-party plugins as an extension, I cannot control all third-party code.

I have also tried the following code, but if an exception occurs in Canvas, the whole program will still exit.

window.exceptionHandler = WindowExceptionHandler { it.printStackTrace() }

ShirasawaSama avatar Dec 28 '22 20:12 ShirasawaSama

@dima-avdeev-jb Hello, this issue is not only for the web platform, but also for the desktop/JVM and android/ios.

ShirasawaSama avatar Jan 03 '23 00:01 ShirasawaSama

We don't have such a component. And it's hard to implement. But, you may provide a special interface and wrap third party libraries.

For example:

interface IntegrateLibrary {
    val networkContext: CoroutineContext
    fun <T> tryCatchNonComposableLogic(logic: () -> T): T?
}

@Composable
fun IntegrateThirdPartyLibrary(libraryContent: @Composable IntegrateLibrary.() -> Unit) {
    val errorLogs: MutableState<List<String>> = remember { mutableStateOf(emptyList()) }
    if (errorLogs.value.isNotEmpty()) {
        Text("Error occurs:\n ${errorLogs.value.joinToString("\n")}")
    } else {
        val libraryContext = object : IntegrateLibrary {
            override val networkContext: CoroutineContext =
                Dispatchers.IO + CoroutineExceptionHandler { context, throwable ->
                    errorLogs.value = errorLogs.value + throwable.toString()
                }
            override fun <T> tryCatchNonComposableLogic(logic: () -> T): T? {
                try {
                    return logic()
                } catch (throwable: Throwable) {
                    errorLogs.value = errorLogs.value + throwable.toString()
                    return null
                }
            }
        }
        libraryContext.libraryContent()
    }
}

@Composable
fun Usage() {
    IntegrateThirdPartyLibrary {
        val possibleFailedResult = tryCatchNonComposableLogic {
            1 / Random.nextInt(0, 2)
        }
        LaunchedEffect(Unit) {
            withContext(networkContext) {
                // Your network request
            }
        }
    }
}

dima-avdeev-jb avatar Jan 03 '23 09:01 dima-avdeev-jb

@dima-avdeev-jb Thank you very much for your detailed reply.

Yes, I know that exceptions in the NON composing phase can be caught directly.

However, in a large workstation type program, exceptions generated by third-party plugins are possible at any stage.

If a third party can easily interrupt the entire program, the robustness of the program is too bad.

Imagine that if a jetbrains IDEA loads a plugin, it divides by 0 (in UI), and then the entire program exits without saving the user's operation.

But I wonder if it is possible to create multiple composer instances to interrupt local components (Not the whole program)? Is it possible to wrap by the SwingPanel outside?

ShirasawaSama avatar Jan 03 '23 10:01 ShirasawaSama

Do you need this only on Compose Desktop with JVM? If yes, then you can use ClassLoader feature to run potentially unsafe code. And you can create every loaded component with Compose in separate Swing panels. (https://github.com/JetBrains/compose-jb/blob/master/tutorials/Swing_Integration/README.md#using-composepanel) But in this case, you can't use your third party components right inside Composable functions.

dima-avdeev-jb avatar Jan 03 '23 11:01 dima-avdeev-jb

@dima-avdeev-jb Is this your suggested code? I've tried it before, but it still interrupts the whole program.

SwingPanel(Color.Red, { ComposePanel().apply {
    setContent {
        Text("Test")
        Row {
            Text("Test2")
            Box {
                throw RuntimeException("Test error!")
            }
        }
    }
} }, Modifier.fillMaxWidth())

I don't know if I can create an exclusive composer instance for a swing panel in this case.

ShirasawaSama avatar Jan 03 '23 11:01 ShirasawaSama

You can load this SwingPanel with ClassLoader for isolation. Do you need this behaviour only for Desktop JVM?

dima-avdeev-jb avatar Jan 03 '23 11:01 dima-avdeev-jb

Thank you very much. I will try the method you provided later today.

For now, my app is only running on JVM and may be available on Android in a few months.

However, in a word, I think one exception can interrupt the whole program, which will reduce the stability of the program. Especially for workstation software.

ShirasawaSama avatar Jan 03 '23 11:01 ShirasawaSama

It's harder to use ClassLoader on Android for isolation. Maybe not possible.

dima-avdeev-jb avatar Jan 03 '23 11:01 dima-avdeev-jb

Can this be achieved by providing a method for manually creating and destroying compose instances? For example, multiple react instances can be created on a single page in the web.

ShirasawaSama avatar Jan 03 '23 11:01 ShirasawaSama

On Android, you can control the lifecycle of Compose inside ComposeView. And manually create them. https://developer.android.com/jetpack/compose/interop/interop-apis

dima-avdeev-jb avatar Jan 03 '23 12:01 dima-avdeev-jb

Can you provide a code sample of using ClassLoader to call a compose function?

ShirasawaSama avatar Jan 03 '23 12:01 ShirasawaSama

I will try to do it later. But, with ClassLoader, it is not possible to directly call Composable functions. It's possible to create new Swing panels with precompiled Composable functions inside.

dima-avdeev-jb avatar Jan 03 '23 12:01 dima-avdeev-jb

Ok, thank you.

ShirasawaSama avatar Jan 03 '23 12:01 ShirasawaSama

@dima-avdeev-jb Hello, is there any progress?

ShirasawaSama avatar Feb 21 '23 00:02 ShirasawaSama

@ShirasawaSama Sorry, I don't have enough time for now. I will try to make a sample after the KotlinConf event. Can you please ping me after 15th April?

dima-avdeev-jb avatar Feb 21 '23 19:02 dima-avdeev-jb

@ShirasawaSama Sorry, I don't have enough time for now. I will try to make a sample after the KotlinConf event. Can you please ping me after 15th April?

Ok. And thank you very much for your reply

ShirasawaSama avatar Feb 22 '23 00:02 ShirasawaSama

But I wonder if it is possible to create multiple composer instances to interrupt local components

I believe that for a proper isolation, we should provide a way to create multiple composer instances.

pjBooms avatar Feb 22 '23 09:02 pjBooms

@dima-avdeev-jb Hello, I would like to ask if you have time to deal with this issue?

ShirasawaSama avatar Apr 25 '23 08:04 ShirasawaSama

Yes, I will try to make a sample on this week

dima-avdeev-jb avatar Apr 25 '23 14:04 dima-avdeev-jb

I made a sample here: https://github.com/dima-avdeev-jb/compose-with-classloader

dima-avdeev-jb avatar Apr 26 '23 17:04 dima-avdeev-jb

@dima-avdeev-jb Thank you very much for the sample code, I will test and use it soon.

But for me, I'd rather have a way to handle and recover from local component errors.

ShirasawaSama avatar Apr 27 '23 10:04 ShirasawaSama

Hi there!

I also have a use-case for this, where a native try-catch mechanism will be super helpful.

So I have a font previewing app, where users can select any font file from their device, and I apply it to a BasicTextField.

I do some validation if the font is valid at all (using a native library), and then also validate if Android can open it at all (because sometimes, I noticed that Android doesn't support some fonts. Same with desktop) using FontFamilyResolver.resolve() in a try-catch.

However, despite all of these measures before changing the fontFamily state of the BasicTextField, I still see crashes happening in production due to some internal code in BasicTextField crashing (seemingly FontFamilyResolver.resolve is successful, but actually loading some fonts causes other issues).

This causes the entire app to crash, and there's no way for me to catch such issues and properly inform the user that the font is unsupported.

I'm not sure, but this could also be useful when Compose Runtime / Compiler are used for data libraries (e.g. Molecule), but I don't know much about that.

I remember some Google employee saying this was a planned thing for the future, but there's basically no news about it.

Skaldebane avatar Feb 14 '24 07:02 Skaldebane

@Skaldebane Thanks for your use case!

dima-avdeev-jb avatar Feb 15 '24 05:02 dima-avdeev-jb

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

okushnikov avatar Jul 14 '24 15:07 okushnikov