Errors
In Javascript, errors are thrown when an error condition is found.
When calling code which can throw, it is good practice to wrap that code in a try
/catch
block:
try {
const result = divide(42, 0); // throws
} catch (e: any) {
// do something with the error.
}
In other languages, e.g. Java or Swift, the method that can throw must declare it on the method signature. e.g.
In Java:
float divide(float top, float bottom) throws MathException {}
while in Swift:
func divide(top: Float, bottom: Float) throws -> Float {}
In Rust, instead of throwing with try/catch, a method returns a Result
enum.
#![allow(unused)] fn main() { #[derive(uniffi::Error)] pub enum MathError { DivideByZero, NumberOverflow, } #[uniffi::export] fn divide(top: f64, bottom: f64) -> Result<f64, MathError> { if bottom == 0.0 { Err(MathError::DivideByZero) } else { Ok(top / bottom) } } }
Enums as Errors
Notice that MathError
is not itself a special kind of object. In idiomatic Rust, this is usually an enum.
uniffi-bindgen-react-native
converts these types of enums-as-errors in to JS Errors. Due to a limitation in babel
, subclasses of Error
do not evaluate instanceof
as expected. For this reason, each variant has its own instanceOf
static method.
try {
divide(x, y);
} catch (e: any) {
if (MathError.instanceOf(e)) {
e instanceof Error; // true
e instanceof MathError; // false
}
if (MathError.DivideByZero.instanceOf(e)) {
// handle divide by zero
}
}
Such enums as errors, without properties also have a companion _Tags
enum.
Using a switch
on the error’s tag
property is a convenient way of handling all cases:
try {
divide(x, y);
} catch (e: any) {
if (MathError.instanceOf(e)) {
switch (e.tag) {
case MathError_Tags.DivideByZero: {
// handle divide by zero
break;
}
case MathError_Tahs.NumberOverflow: {
// handle overflow
break;
}
}
}
}
Enums with properties as Errors
Enums-as-errors may also have properties. These are exactly the same as other enums with properties, except they subclass Error
.
e.g.
#![allow(unused)] fn main() { enum MyRequestError { UrlParsing(String), Timeout { timeout: u32 }, ConnectionLost, } #[uniffi::export] fn make_request() -> Result<String, MyRequestError> { // dummy implmentation. return Err(MyRequestError::ConnectionLost); } }
In typescript:
try {
makeRequest();
} catch (e: any) {
if (MyRequestError.instanceOf(e)) {
switch (e.tag) {
case MyRequestError_Tags.UrlParsing: {
console.error(`Url is bad ${e.inner[0]}!`);
break;
}
case MyRequestError_Tags.Timeout: {
const { timeout } = e.inner;
console.error(`Timeout after ${timeout} seconds!`);
break;
}
case MyRequestError_Tags.ConnectionLost {
console.error(`Connection lost!`);
break;
}
}
}
}
Flat errors
A common pattern in Rust is to convert enum properties to a message. Uniffi calls these error enums flat_errors
.
In this example, a MyError::InvalidDataError
has no properties but gets the message "Invalid data"
, ParseError
converts its properties in to a message, and JSONError
takes any serde_json::Error
to make a JSONError
, which then gets converted to a string.
In this case, the conversion is being managed by the thiserror
crate’s macros.
#![allow(unused)] fn main() { #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] pub enum MyError { // A message from a variant with no properties #[error("Invalid data")] InvalidDataError, // A message from a variant with named properties #[error("Parse error at line {line}, column {col}")] ParseError { line: usize, col: usize }, // A message from an JSON error, converted into a MyError #[error("JSON Error: {0}")] JSONError(#[from] serde_json::Error), } }
Unlike flat enums, flat errors have a tag
property and a companion MyError_Tags
enum.
These can be handled in typescript like so:
try {
// … do sometihng that throws
} catch (err: any) {
if (MyError.instanceOf(err)) {
switch (err.tag) {
case MyError_Tags.InvalidDataError: {
// e.message will be "MyError.InvalidDataError: Invalid data"
break;
}
case MyError_Tags.ParseError: {
// e.message will be paramterized, e.g.
// "MyError.ParseError: Parse error at line 12, column 4"
break;
}
case MyError_Tags.JSONError: {
// e.message will be a wrapped serde_json error, e.g.
// "MyError.JSONError: Expected , \" or \]"
break;
}
}
}
}
Objects as Errors
As you may have gathered, in Rust errors can be anything including objects. In the rare occasions this may be useful:
#![allow(unused)] fn main() { #[derive(uniffi::Object)] pub struct MyErrorObject { e: String, } #[uniffi::export] impl MyErrorObject { fn message_from_rust(&self) -> String { self.e.clone() } } #[uniffi::export] fn throw_object(message: String) -> Result<(), MyErrorObject> { Err(MyErrorObject { e: message }) } }
This is used in Typescript, the error itself is not the object.
try {
throwObject("a message")
} catch (e: any) {
if (MyErrorObject.instanceOf(e)) {
// NOPE
}
if (MyErrorObject.hasInner(e)) {
const error = MyErrorObject.getInner(e);
MyErrorObject.instanceOf(error); // true
console.error(error.messageFromRust())
}
}
Rust Error
is renamed as Exception
in typescript
To avoid collisions with the ECMAScript standard Error
, any Rust enums, objects and records called Error
are renamed Exception
.