-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exposing Errors #509
Comments
Option 1One requirement you might have for errors is that their fields be exposed to the bindings. For example, the #[derive(Debug, thiserror::Error)]
pub enum TxidParseError {
#[error("invalid txid: {txid}")]
InvalidTxid { txid: String },
} In the UDL, this shows up as: [Error]
interface TxidParseError {
InvalidTxid(string txid);
}; When building bindings code for this however, the resulting exception thrown will not use the message you might expect (the one built by sealed class TxidParseException: Exception() {
class InvalidTxid(val txid: String) : TxidParseException() {
override val message
get() = "txid=${ `txid` }"
} This means that we cannot customize the message given to the user; the invalid HTTP header value: {value} You only send back to the Koltin/Swift user an exception with a field value=${value} This is inconvenient, and overall doesn't provide the sort of information one might want to convey. We really want to customize our error messages, but we also need to bring in our data! This leads us to option 2. Option 2Option 2 relies on using interfaces as error. In this scenario, your errors will be interfaces, which means they will not have any fields. You can instead define a impl MyError {
fn message(&self) -> String> { self.to_string() }
} This appears better at first glance, but is a little weird on the user side of things, because now your errors don't have the ever so common (at least in Kotlin?) Option 3The third option requires using the enum directly without any fields: [Error]
enum FeeRateError {
"ArithmeticOverflow"
}; In this situation, the sealed class FeeRateException(message: String): Exception(message) {
class ArithmeticOverflow(message: String) : FeeRateException(message) This means that we can now leverage the #[derive(Debug, thiserror::Error)]
pub enum FeeRateError {
#[error("arithmetic overflow on feerate")]
ArithmeticOverflow,
} At first glance, the downside of this is that no fields can be passed forward (the enum doesn't have any fields in the target languages). But we can actually use fields on the Rust side to create messages that contain that data once transformed into strings! Not perfect, but it might be better than the other options above. #[derive(Debug, thiserror::Error)]
pub enum FeeRateError {
#[error("arithmetic overflow on feerate {e}")]
ArithmeticOverflow { e: u64 },
} Option 4If you use a struct as an error type instead of an enum, your struct can implement the Display trait which will populate the This is maybe less applicable for us because it means your errors are not variants of an enum and are simply objects by themselves. It turns out you can combine them into an enum if you want I think, but then you loose the nice Display features. |
This is an awesome breakdown. 1 or 3 definitely are our best bets. |
Ok here are a few more data points. In Swift/Kotlin, the resulting Errors/Exceptions are actually the same whether you choose option 1 or option 2! This is really interesting, because it means that while we think using 2 approaches to errors might feel messy and confuse the end user, this is not the case; the complexity only lies at the ffi layer. We have to know that we build errors in two different ways, but the end user sees the same structures. Note that for Kotlin/Javausers, they get a Rust code: #[derive(Debug, thiserror::Error)]
// Using Option 3
pub enum FeeRateError {
#[error("arithmetic overflow on feerate")]
ArithmeticOverflow,
#[error("invalid feerate {e}")]
InvalidFeeRate { e: u64 },
}
// Using Option 1
#[derive(Debug, thiserror::Error)]
pub enum TxidParseError {
#[error("invalid txid: {txid}")]
InvalidTxid { txid: String, error_message: String },
} UDL file: [Error]
enum FeeRateError {
"ArithmeticOverflow",
"InvalidFeeRate"
};
[Error]
interface TxidParseError {
InvalidTxid(string txid, string error_message);
}; Kotlinsealed class FeeRateException(message: String): Exception(message) {
class ArithmeticOverflow(message: String) : FeeRateException(message)
class InvalidFeeRate(message: String) : FeeRateException(message)
}
sealed class TxidParseException: Exception() {
class InvalidTxid(
val txid: String,
val errorMessage: String
) : TxidParseException() {
override val message
get() = "txid=${txid}, errorMessage=${errorMessage}"
}
} Swiftpublic enum FeeRateError {
case ArithmeticOverflow(message: String)
case InvalidFeeRate(message: String)
}
extension FeeRateError: Equatable, Hashable {}
extension FeeRateError: Error { }
public enum TxidParseError {
case InvalidTxid(
txid: String,
errorMessage: String
)
}
extension TxidParseError: Equatable, Hashable {}
extension TxidParseError: Error { } |
So it turns out there is a 4th option. If you use a struct as an error type instead of an enum, your struct can implement the Display trait which will populate the This is maybe less applicable for us because it means your errors are not variants of an enum and are simply objects by themselves. It turns out you can combine them into an enum if you want I think, but then you loose the nice Display features. I'm adding this to the post above just to keep all options in one big comment. |
I prefer option 1 because it give apps access to the error data with which they can construct their own user facing errors, as long as we have good error docs that should be enough context for developers. If we go with option 3 we get the nice english error message strings, but those would need to be parsed somehow to get to the data so would be hard to localize and would be less usable in an app UI. |
I just added a comment to our ErrorADR a few mins ago that I think aligns with this general concept you're describing: #513 (comment) |
It turns out that errors are not easy to move from Rust to the target languages when using uniffi. In short, it looks like one must choose between a range of options, each with pros and cons, and none particularly perfect. This issue is an attempt at quantifying and categorizing these options and the choices we made for the bdk-ffi language bindings.
The text was updated successfully, but these errors were encountered: