Skip to content

Commit

Permalink
Rethrow fatal errors on JVM
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Jan 29, 2025
1 parent e46d432 commit 9982258
Show file tree
Hide file tree
Showing 14 changed files with 767 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.badoo.reaktive.base

import com.badoo.reaktive.base.exceptions.CompositeException
import com.badoo.reaktive.utils.handleReaktiveError
import com.badoo.reaktive.utils.throwIfFatal

inline fun <T> ErrorCallback.tryCatch(
block: () -> T,
Expand All @@ -10,7 +12,18 @@ inline fun <T> ErrorCallback.tryCatch(
try {
block()
} catch (e: Throwable) {
handleReaktiveError(errorTransformer(e), ::onError)
e.throwIfFatal()

val transformedError =
try {
errorTransformer(e)
} catch (e2: Throwable) {
e2.throwIfFatal()
CompositeException(cause1 = e, cause2 = e2)
}

handleReaktiveError(transformedError, ::onError)

return
}
.also(onSuccess)
Expand All @@ -23,7 +36,17 @@ inline fun ErrorCallback.tryCatch(
try {
block()
} catch (e: Throwable) {
handleReaktiveError(errorTransformer(e), ::onError)
e.throwIfFatal()

val transformedError =
try {
errorTransformer(e)
} catch (e2: Throwable) {
e2.throwIfFatal()
CompositeException(cause1 = e, cause2 = e2)
}

handleReaktiveError(transformedError, ::onError)
}
}

Expand All @@ -34,6 +57,16 @@ internal inline fun tryCatchAndHandle(
try {
block()
} catch (e: Throwable) {
handleReaktiveError(errorTransformer(e))
e.throwIfFatal()

val transformedError =
try {
errorTransformer(e)
} catch (e2: Throwable) {
e2.throwIfFatal()
CompositeException(cause1 = e, cause2 = e2)
}

handleReaktiveError(transformedError)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.badoo.reaktive.utils
import com.badoo.reaktive.base.exceptions.CompositeException

fun handleReaktiveError(error: Throwable, onError: ((Throwable) -> Unit)? = null) {
error.throwIfFatal()

if (onError == null) {
handleError(error)
} else {
Expand All @@ -14,6 +16,7 @@ private fun handleError(error: Throwable) {
try {
reaktiveUncaughtErrorHandler(error)
} catch (errorDeliveryException: Throwable) {
errorDeliveryException.throwIfFatal()
printErrors("Error delivering uncaught error", error, errorDeliveryException)
}
}
Expand All @@ -22,11 +25,13 @@ private fun handleError(error: Throwable, onError: (Throwable) -> Unit) {
try {
onError(error)
} catch (errorHandlerException: Throwable) {
errorHandlerException.throwIfFatal()
printErrors("onError callback failed", error, errorHandlerException)

try {
reaktiveUncaughtErrorHandler(CompositeException(error, errorHandlerException))
} catch (errorDeliveryException: Throwable) {
errorDeliveryException.throwIfFatal()
printErrors("Error delivering uncaught error", error, errorDeliveryException)
}
}
Expand All @@ -37,3 +42,11 @@ private fun printErrors(message: String, outerError: Throwable, innerError: Thro
outerError.printStackTrace()
innerError.printStackTrace()
}

fun Throwable.throwIfFatal() {
if (isFatal()) {
throw this
}
}

internal expect fun Throwable.isFatal(): Boolean
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.badoo.reaktive.test

import com.badoo.reaktive.base.ErrorCallback

class TestErrorCallback(
private val onError: (Throwable) -> Unit = {},
) : ErrorCallback {

var error: Throwable? = null

override fun onError(error: Throwable) {
onError.invoke(error)
this.error = error
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.badoo.reaktive.utils

import com.badoo.reaktive.base.exceptions.CompositeException
import com.badoo.reaktive.base.tryCatchAndHandle
import com.badoo.reaktive.test.mockUncaughtExceptionHandler
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertSame

class TryCatchAndHandleTest {

@AfterTest
fun after() {
resetReaktiveUncaughtErrorHandler()
}

@Test
fun calls_reaktiveUncaughtErrorHandler_WHEN_block_thrown_exception() {
val caughtException = mockUncaughtExceptionHandler()
val error = Exception()

tryCatchAndHandle { throw error }

assertSame(error, caughtException.value)
}

@Test
fun calls_reaktiveUncaughtErrorHandler_WHEN_errorTransformer_thrown_exception() {
val caughtException = mockUncaughtExceptionHandler()
val error1 = Exception()
val error2 = Exception()

tryCatchAndHandle(
errorTransformer = { throw error2 },
block = { throw error1 },
)

val error = caughtException.value
assertIs<CompositeException>(error)
assertSame(error1, error.cause1)
assertSame(error2, error.cause2)
}

@Test
fun swallows_WHEN_reaktiveUncaughtErrorHandler_thrown_exception() {
reaktiveUncaughtErrorHandler = { throw Exception() }
tryCatchAndHandle { throw Exception() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.badoo.reaktive.utils

import com.badoo.reaktive.base.exceptions.CompositeException
import com.badoo.reaktive.base.tryCatch
import com.badoo.reaktive.test.TestErrorCallback
import com.badoo.reaktive.test.mockUncaughtExceptionHandler
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertSame

class TryCatchTest {

@AfterTest
fun after() {
resetReaktiveUncaughtErrorHandler()
}

@Test
fun calls_ErrorCallback_WHEN_block_thrown_exception() {
val errorCallback = TestErrorCallback()
val error = Exception()

errorCallback.tryCatch { throw error }

assertSame(error, errorCallback.error)
}

@Test
fun calls_ErrorCallback_WHEN_errorTransformer_thrown_exception() {
val errorCallback = TestErrorCallback()
val error1 = Exception()
val error2 = Exception()

errorCallback.tryCatch(
errorTransformer = { throw error2 },
block = { throw error1 },
)

val error = errorCallback.error
assertIs<CompositeException>(error)
assertSame(error1, error.cause1)
assertSame(error2, error.cause2)
}

@Test
fun calls_reaktiveUncaughtErrorHandler_WHEN_ErrorCallback_thrown_exception() {
val caughtException = mockUncaughtExceptionHandler()
val error1 = Exception()
val error2 = Exception()

val errorCallback = TestErrorCallback { throw error2 }
errorCallback.tryCatch { throw error1 }

val error = caughtException.value
assertIs<CompositeException>(error)
assertSame(error1, error.cause1)
assertSame(error2, error.cause2)
}

@Test
fun swallows_WHEN_reaktiveUncaughtErrorHandler_thrown_exception() {
reaktiveUncaughtErrorHandler = { throw Exception() }
val errorCallback = TestErrorCallback { throw Exception() }
errorCallback.tryCatch { throw Exception() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.badoo.reaktive.utils

import com.badoo.reaktive.base.exceptions.CompositeException
import com.badoo.reaktive.base.tryCatch
import com.badoo.reaktive.test.TestErrorCallback
import com.badoo.reaktive.test.mockUncaughtExceptionHandler
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertSame

class TryCatchWithOnSuccessTest {

@AfterTest
fun after() {
resetReaktiveUncaughtErrorHandler()
}

@Test
fun calls_onSuccess_WHEN_block_not_thrown() {
val errorCallback = TestErrorCallback()
var result: Int? = null

errorCallback.tryCatch(block = { 0 }, onSuccess = { result = 0 })

assertEquals(0, result)
}

@Test
fun calls_ErrorCallback_WHEN_block_thrown_exception() {
val errorCallback = TestErrorCallback()
val error = Exception()

errorCallback.tryCatch(block = { throw error }, onSuccess = {})

assertSame(error, errorCallback.error)
}

@Test
fun calls_ErrorCallback_WHEN_errorTransformer_thrown_exception() {
val errorCallback = TestErrorCallback()
val error1 = Exception()
val error2 = Exception()

errorCallback.tryCatch(
block = { throw error1 },
errorTransformer = { throw error2 },
onSuccess = {},
)

val error = errorCallback.error
assertIs<CompositeException>(error)
assertSame(error1, error.cause1)
assertSame(error2, error.cause2)
}

@Test
fun calls_reaktiveUncaughtErrorHandler_WHEN_ErrorCallback_thrown_exception() {
val caughtException = mockUncaughtExceptionHandler()
val error1 = Exception()
val error2 = Exception()

val errorCallback = TestErrorCallback { throw error2 }
errorCallback.tryCatch(block = { throw error1 }, onSuccess = {})

val error = caughtException.value
assertIs<CompositeException>(error)
assertSame(error1, error.cause1)
assertSame(error2, error.cause2)
}

@Test
fun swallows_WHEN_reaktiveUncaughtErrorHandler_thrown_exception() {
reaktiveUncaughtErrorHandler = { throw Exception() }
val errorCallback = TestErrorCallback { throw Exception() }
errorCallback.tryCatch(block = { throw Exception() }, onSuccess = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.badoo.reaktive.utils

actual fun Throwable.isFatal(): Boolean = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.badoo.reaktive.utils

internal actual fun Throwable.isFatal(): Boolean =
(this is VirtualMachineError) ||
(this is ThreadDeath) ||
(this is LinkageError)
Loading

0 comments on commit 9982258

Please sign in to comment.