Skip to content

Commit 8ba04bc

Browse files
committed
Allow CopyableThrowable to modify exception message and document message identity in debugging.md
Addresses #1931
1 parent 41e2cf9 commit 8ba04bc

File tree

4 files changed

+35
-3
lines changed

4 files changed

+35
-3
lines changed

docs/topics/debugging.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ Exception copy logic is straightforward:
6363
1) If the exception class implements [CopyableThrowable], [CopyableThrowable.createCopy] is used.
6464
`null` can be returned from `createCopy` to opt-out specific exception from being recovered.
6565
2) If the exception class has class-specific fields not inherited from Throwable, the exception is not copied.
66-
3) Otherwise, one of the public exception's constructor is invoked reflectively with an optional `initCause` call.
66+
3) Otherwise, one of the public exception's constructor is invoked reflectively with an optional `initCause` call.
67+
4) If the reflective copy has a changed message (exception constructor passed a modified `message` parameter to the superclass),
68+
the exception is not copied in order to preserve a human-readable message. [CopyableThrowable] does not have such a limitation
69+
and allows the copy to have a `message` different from that of the original.
6770

6871
## Debug agent
6972

kotlinx-coroutines-core/common/src/Debug.common.kt

+5
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ public interface CopyableThrowable<T> where T : Throwable, T : CopyableThrowable
3232

3333
/**
3434
* Creates a copy of the current instance.
35+
*
3536
* For better debuggability, it is recommended to use original exception as [cause][Throwable.cause] of the resulting one.
3637
* Stacktrace of copied exception will be overwritten by stacktrace recovery machinery by [Throwable.setStackTrace] call.
3738
* An exception can opt-out of copying by returning `null` from this function.
3839
* Suppressed exceptions of the original exception should not be copied in order to avoid circular exceptions.
40+
*
41+
* This function is allowed to create a copy with a modified [message][Throwable.message], but it should be noted
42+
* that the copy can be later recovered as well and message modification code should handle this situation correctly
43+
* (e.g. by also storing the original message and checking it) to produce a human-readable result.
3944
*/
4045
public fun createCopy(): T?
4146
}

kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ private fun <E : Throwable> recoverFromStackFrame(exception: E, continuation: Co
8181
private fun <E : Throwable> tryCopyAndVerify(exception: E): E? {
8282
val newException = tryCopyException(exception) ?: return null
8383
// Verify that the new exception has the same message as the original one (bail out if not, see #1631)
84-
if (newException.message != exception.message) return null
84+
// CopyableThrowable has control over its message and thus can modify it the way it wants
85+
if (exception !is CopyableThrowable<*> && newException.message != exception.message) return null
8586
return newException
8687
}
8788

kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt

+24-1
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,39 @@ class StackTraceRecoveryCustomExceptionsTest : TestBase() {
8989

9090
@Test
9191
fun testWrongMessageExceptionInChannel() = runTest {
92-
// Separate code path
9392
val result = produce<Unit>(SupervisorJob() + Dispatchers.Unconfined) {
9493
throw WrongMessageException("OK")
9594
}
9695
val ex = runCatching {
96+
@Suppress("ControlFlowWithEmptyBody")
9797
for (unit in result) {
9898
// Iterator has a special code path
9999
}
100100
}.exceptionOrNull() ?: error("Expected to fail")
101101
assertTrue(ex is WrongMessageException)
102102
assertEquals("Token OK", ex.message)
103103
}
104+
105+
class CopyableWithCustomMessage(
106+
message: String?,
107+
cause: Throwable? = null
108+
) : RuntimeException(message, cause),
109+
CopyableThrowable<CopyableWithCustomMessage> {
110+
111+
override fun createCopy(): CopyableWithCustomMessage {
112+
return CopyableWithCustomMessage("Recovered: [$message]", cause)
113+
}
114+
}
115+
116+
@Test
117+
fun testCustomCopyableMessage() = runTest {
118+
val result = runCatching {
119+
coroutineScope<Unit> {
120+
throw CopyableWithCustomMessage("OK")
121+
}
122+
}
123+
val ex = result.exceptionOrNull() ?: error("Expected to fail")
124+
assertTrue(ex is CopyableWithCustomMessage)
125+
assertEquals("Recovered: [OK]", ex.message)
126+
}
104127
}

0 commit comments

Comments
 (0)