Skip to content

Export Display trait on errors#1021

Open
thunderbiscuit wants to merge 1 commit into
bitcoindevkit:masterfrom
thunderbiscuit:feat/better-errors
Open

Export Display trait on errors#1021
thunderbiscuit wants to merge 1 commit into
bitcoindevkit:masterfrom
thunderbiscuit:feat/better-errors

Conversation

@thunderbiscuit

@thunderbiscuit thunderbiscuit commented Jun 3, 2026

Copy link
Copy Markdown
Member

This brings the Display trait we create using the thiserror::Error macro to the bindings. We've been implementing the Display trait for all this time, but not passing it on to the bindings!

Note that the draft version of this PR implements this just for the CreateWithPersistError enum.

See related issues #509 and #964.

Todo

  • Export Display and Debug on all errors
  • Look at the messages to make sure they are what we want (for example I think a nice addition would be to add quotes around the message given from upstream, so that the string reads: http error with status code 404 and message 'Block not found' instead of http error with status code 404 and message Block not found. Just an idea.

In Practice

Here is what this means in practice:

Kotlin

The default toString() method on Exceptions (className + ": " + message) is overriden by the Display trait. You loose the className when the exception is printed out, but you gain the human-readable message we maintain in the crate. If you want the Exception type, you can still print it. You keep the strongly typed fields (in the example below your status field is a UShort), and the message field is just a stringified concatenation of all the fields. This means you have to know that on BDK exceptions, the human-readable message is not in fact in the message field like you expect on typical Kotlin errors, but instead in the toString() method.

@Test  
fun `Esplora header height exception`() {  
    try {  
        val esploraClient = EsploraClient(ESPLORA_REGTEST_URL)  
        esploraClient.getBlockHash(10000000u)  
    } catch (e: Exception) {
	    println("Type:     ${e::class.qualifiedName}") 
        println("Message:  ${e.message}")  
        println("toString: ${e.toString()}")  
    }
}
// The exception type is:
// org.bitcoindevkit.EsploraException.HttpResponse

// The exception.message field is:
// status=404, errorMessage=Block not found
   
// The exception.toString() method returns:
// http error with status code 404 and message Block not found

Swift

In Swift, two new fields are added to the errors: description and debugDescription. The description field now holds the human-readable messages we craft on the structs using thiserror. The `debugDescription is just the Debug trait on Rust. Note that this is not available in Kotlin, as there is not equivalent "debug" print method or field.

    func testNewAddress() throws {
        do {
            let persister = try Persister.newInMemory()
            let _ = try Wallet(
                descriptor: descriptor,
                changeDescriptor: changeDescriptor,
                network: .regtest,
                persister: persister
            )
        } catch let error as CreateWithPersistError {
        print("\n========================================")
        print("🔴 CAUGHT ERROR")
        print("========================================")
        print("print(error):         \(error)")
        print("----------------------------------------")
        print("localizedDescription: \(error.localizedDescription)")
        print("----------------------------------------")
        print("errorDescription:     \(error.errorDescription)")
        print("----------------------------------------")
        print("description:          \(error.description)")
        print("----------------------------------------")
        print("debugDescription:     \(error.debugDescription)")
        print("----------------------------------------")
        print("dump(error):")
        dump(error)
        print("========================================\n")
        throw error
        }
    }
========================================
🔴 CAUGHT ERROR
========================================
print(error):         Descriptor(errorMessage: "External and internal descriptors are the same")
----------------------------------------
localizedDescription: BitcoinDevKit.CreateWithPersistError.Descriptor(errorMessage: "External and internal descriptors are the same")
----------------------------------------
errorDescription:     Optional("BitcoinDevKit.CreateWithPersistError.Descriptor(errorMessage: \"External and internal descriptors are the same\")")
----------------------------------------
description:          the loaded changeset cannot construct wallet: External and internal descriptors are the same
----------------------------------------
debugDescription:     Descriptor { error_message: "External and internal descriptors are the same" }
----------------------------------------
dump(error):
▿ BitcoinDevKit.CreateWithPersistError.Descriptor
  ▿ Descriptor: (1 element)
    - errorMessage: "External and internal descriptors are the same"
========================================

Changelog

## Added
- [Swift]: Errors get new `description` and `debugDescription` fields. `description` defers to the Display trait and `debugDescription` defers to the `Debug` trait.
- [Kotlin]: Exceptions now use a customized `toString()` method with a human-readable message populated by the `Display` trait.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing
  • I've added exactly one changelog:* label
  • I've linked the relevant upstream docs or specs above

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

@ItoroD

ItoroD commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

@thunderbiscuit This is ineresting for the swift side I can see the difference this is making. For kotlin, are you saying we will have to implement a custom display method (Just to clarify).

Edit - I guess not cause that will then affect all bindings.

@reez

reez commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Concept ACK

Comment thread bdk-ffi/src/error.rs
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi::export(Debug, Display)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the PR title/changelog describe exporting Display on errors broadly, should this export be applied to the other uniffi::Error enums in this file too, or should the PR scope be narrowed to CreateWithPersistError only? Right now this is the only error type getting Debug/Display exported to the bindings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants