-
Notifications
You must be signed in to change notification settings - Fork 67
Description
I'm writing one more chapter into the uniffi errors saga. See #509 for prior work on the subject.
Kotlin
In Kotlin, you expect errors to behave like so:
class MySimpleException(override val message: String?) : Exception()
try {
throw MySimpleException("This is an exception my friend!")
} catch (e: Throwable) {
println("The full exception: $e")
println("The message: ${e.message}")
println("The localized message: ${e.localizedMessage}")
println("The cause: ${e.cause}")
println("The stack trace: ${e.printStackTrace()}")
}
// The full error: org.bitcoindevkit.MySimpleException: This is an exception my friend!
// The message: This is an exception my friend!
// The localized message: This is an exception my friend!
// The cause: null
// org.bitcoindevkit.MySimpleException: This is an exception my friend!
// at org.bitcoindevkit.ErrorsTest.myCustomError(ErrorsTest.kt:53)
// at java.lang.reflect.Method.invoke(Native Method)
// at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
// at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
// at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)As of uniffi 0.30.0, a Rust error like so:
#[derive(Debug, uniffi::Error, thiserror::Error)]
#[uniffi::export(Display)]
pub enum AddressParseError {
#[error("witness version conversion/parsing error: {error_message}")]
WitnessVersion { error_message: String },
#[error("segwit address bech32 string")]
InvalidLegacyPrefix,
#[error("validation error")]
NetworkValidation,
}Turns into a Kotlin exception with an empty message field, except for variants that have fields (like the WitnessVersion above), where the fields get stringified and put into the message variable.
Exporting the Display trait doesn't populate the toString() method (bug in uniffi but only for Kotlin, the fix shipped in 0.31.0). But even with the toString implemented, the actual implementation we'd currently get if we bumped to 0.31.0 is weird, because it erases the type from the toString on the type and only keeps the message. So for example given the above we get:
try {
Address(
address = "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2",
network = Network.REGTEST
)
} catch (e: Throwable) {
println("The full exception: $e")
println("The message: ${e.message}")
println("The localized message: ${e.localizedMessage}")
println("The cause: ${e.cause}")
println("The stack trace: ${e.printStackTrace()}")
}
// The full exception: validation error
// The message:
// The localized message:
// The cause: null
// validation error
// at org.bitcoindevkit.FfiConverterTypeAddressParseError.read(bdk.kt:23487)
// at org.bitcoindevkit.FfiConverterTypeAddressParseError.read(bdk.kt:23470)Where the AddressParseError.NetworkValidation is erased from the toString().
One solution to this would be to remove our dependency on thiserror and write the Display traits ourselves, which would allow us to keep the toString cleaner.
One issue with this is that Swift probably writes them a bit differently, and so we'd need to balance the expectations of users on both sides.
Swift
In Swift the error also gets garbled but I need to look more into what would be standard/expected from a dev.
At the moment you get the following. You can see that the error message as intended is put into the description field. the other 3 show the error type.
func testLatest() {
do {
let _ = try Address(
address: "tb1qan3lldunh37ma6c0afeywgjyjgnyc8uz975zl2",
network: Network.regtest
)
} catch let error as AddressParseError {
print("The full exception: \(error)")
print("The localized description: \(error.localizedDescription)")
print("The description: \(error.description)")
print("The error description: \(error.errorDescription)")
} catch {
print("Unexpected error type: \(error)")
}
}
// The full exception: NetworkValidation
// The localized description: BitcoinDevKit.AddressParseError.NetworkValidation
// The description: validation error
// The error description: Optional("BitcoinDevKit.AddressParseError.NetworkValidation")After asking Claude if this is standard or expected from a Swift dev, here is the answer (just copy/pasting here)
A Swift developer would likely find this pattern familiar, but the output would surprise them in a couple of ways.
The pattern of catching as a specific error type and using localizedDescription is completely standard. What would feel off is the inconsistency in what the properties return:
- localizedDescription and errorDescription returning BitcoinDevKit.AddressParseError.NetworkValidation is not what a Swift dev would expect — the convention is that these return a human-readable string meant for display, like "validation error". This is what Apple's own LocalizedError documentation emphasizes.
- description returning "validation error" is actually the most useful output here, but a Swift dev wouldn't naturally reach for it first — it's not part of the standard error handling protocols.
So the core issue worth writing about is that the UniFFI-generated code puts the useful human-readable string (from your Rust Display impl) on description, while the standard Swift error properties that developers habitually use — localizedDescription and errorDescription — return the reflection string instead. A Swift dev following normal conventions would never discover description without reading the generated source, and would be stuck with the verbose type-qualified string.
The Kotlin equivalent would be if message returned the class name and you had to know to call some non-standard method to get the actual error text.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status