Skip to content
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

Add tag property to flat errors #117

Merged
merged 1 commit into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
{{- self.import_infra("UniffiError", "errors") }}
{{- self.import_infra("uniffiTypeNameSymbol", "symbols") }}
{{- self.import_infra("variantOrdinalSymbol", "symbols") }}
{%- let instance_of = "instanceOf" %}
{%- let flat = e.is_flat() %}
{%- if flat %}

// Flat error type: {{ decl_type_name }}
{%- let type_name__Tags = format!("{type_name}_Tags") %}
export enum {{ type_name__Tags }} {
{%- for variant in e.variants() %}
{{ variant|variant_name }} = "{{ variant.name() }}"
{%- if !loop.last %},{% endif -%}
{% endfor %}
}

{%- call ts::docstring(e, 0) %}
export const {{ decl_type_name }} = (() => {
{%- for variant in e.variants() %}
Expand All @@ -21,31 +29,30 @@ export const {{ decl_type_name }} = (() => {
* This field is private and should not be used.
*/
readonly [variantOrdinalSymbol] = {{ loop.index }};

public readonly tag = {{ type_name__Tags }}.{{ variant|variant_name }};

constructor(message: string) {
super("{{ type_name }}", "{{ variant_name }}", message);
}

static {{ instance_of }}(e: any): e is {{ variant_name }} {
static instanceOf(e: any): e is {{ variant_name }} {
return (
{{ instance_of }}(e) && (e as any)[variantOrdinalSymbol] === {{ loop.index }}
instanceOf(e) && (e as any)[variantOrdinalSymbol] === {{ loop.index }}
);
}
}
{%- endfor %}

// Utility function which does not rely on instanceof.
function {{ instance_of }}(e: any): e is {# space #}
{%- for variant in e.variants() %}
{{- variant.name()|class_name(ci) }}
{%- if !loop.last %} | {% endif -%}
{%- endfor %} {
function instanceOf(e: any): e is {{ type_name }} {
return (e as any)[uniffiTypeNameSymbol] === "{{ decl_type_name }}";
}
return {
{%- for variant in e.variants() %}
{{ variant.name()|class_name(ci) }},
{%- endfor %}
{{ instance_of }},
instanceOf,
};
})();

Expand Down
83 changes: 82 additions & 1 deletion docs/src/idioms/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ In Rust, instead of throwing with try/catch, a method returns a `Result` enum.
#[derive(uniffi::Error)]
pub enum MathError {
DivideByZero,
NumberOverflow,
}

#[uniffi::export]
Expand Down Expand Up @@ -63,7 +64,32 @@ try {
}
```

Enums-as-errors may also have properties. These are exactly the same as [other enums](./enums.md#enums-with-properties), except they subclass `Error`.
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:

```typescript
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](./enums.md#enums-with-properties), except they subclass `Error`.

e.g.

Expand Down Expand Up @@ -108,6 +134,61 @@ try {

```

#### 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.

```rust
#[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](enums.md#enums-without-properties), flat errors have a `tag` property and a companion `MyError_Tags` enum.

These can be handled in typescript like so:

```typescript
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:
Expand Down
50 changes: 35 additions & 15 deletions fixtures/error-types/tests/bindings/test_error_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
ErrorInterface,
ErrorTrait,
Exception,
Exception_Tags,
FlatInner,
FlatInner_Tags,
getError,
Inner,
oops,
Expand Down Expand Up @@ -97,16 +99,27 @@ test("oopsEnum 2", (t) => {
test("oopsEnum 3", (t) => {
t.assertThrows(
(error) => {
if (Exception.FlatInnerError.instanceOf(error)) {
t.assertEqual(error.toString(), "Error: Exception.FlatInnerError");
t.assertTrue(FlatInner.CaseA.instanceOf(error.inner.error));
t.assertEqual(
error.inner.error.toString(),
"Error: FlatInner.CaseA: inner",
);
// assert(String(describing: e) == "FlatInnerError(error: error_types.FlatInner.CaseA(message: \"inner\"))")
// assert(String(reflecting: e) == "error_types.Error.FlatInnerError(error: error_types.FlatInner.CaseA(message: \"inner\"))")
return true;
if (Exception.instanceOf(error)) {
switch (error.tag) {
case Exception_Tags.FlatInnerError: {
t.assertTrue(Exception.FlatInnerError.instanceOf(error));
t.assertEqual(error.toString(), "Error: Exception.FlatInnerError");
const inner = error.inner.error;
t.assertEqual(inner.toString(), "Error: FlatInner.CaseA: inner");
switch (inner.tag) {
case FlatInner_Tags.CaseA: {
t.assertTrue(FlatInner.CaseA.instanceOf(inner));
t.assertFalse(FlatInner.CaseB.instanceOf(inner));
return true;
}
default: {
// NOOP
}
}
}
default:
// NOOP
}
}
return false;
},
Expand All @@ -120,15 +133,22 @@ test("oopsEnum 4", (t) => {
if (Exception.FlatInnerError.instanceOf(error)) {
t.assertEqual(error.toString(), "Error: Exception.FlatInnerError");
if (Exception.FlatInnerError.hasInner(error)) {
const inner = Exception.FlatInnerError.getInner(error);
if (FlatInner.CaseB.instanceOf(inner)) {
t.assertEqual(inner.error.toString(), "NonUniffiTypeValue: value");
return true;
const innerError = Exception.FlatInnerError.getInner(error);
const inner = innerError.error;
if (FlatInner.instanceOf(inner)) {
switch (inner.tag) {
case FlatInner_Tags.CaseA:
t.assertTrue(FlatInner.CaseA.instanceOf(inner));
return true;
case FlatInner_Tags.CaseB:
// We know that the Rust only produces this variant.
t.assertTrue(FlatInner.CaseB.instanceOf(inner));
return true;
}
}
}
// assert(String(describing: e) == "FlatInnerError(error: error_types.FlatInner.CaseB(message: \"NonUniffiTypeValue: value\"))")
// assert(String(reflecting: e) == "error_types.Error.FlatInnerError(error: error_types.FlatInner.CaseB(message: \"NonUniffiTypeValue: value\"))")
return true;
}
return false;
},
Expand Down