bytecodealliance / bytecodealliance/wasmtime

Add structured error types for programmatic consumption

Open
#3,297 7 comments 2 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
18.6k
Forks
1.8k
Avg merge
1d 19h
Merged PRs (30d)
121

Description

#### Feature

The top-level `wasmtime` crate, used for embedding the wasmtime runtime into Rust applications, should expose structured error types so that callers may react to specific errors that occur within the runtime. Currently, the `wasmtime` API exposes possible failures as an `anyhow::Result`, which is akin to returning a String as an error. The conventional wisdom is that while `anyhow` is great for applications _consuming_ errors, it is more useful for libraries (which _produce_ errors) to define structured errors as enums, potentially using helper crates like `thiserror`. This is even [noted in the anyhow repo]:

> Use Anyhow if you don't care what error type your functions return, you just want it to be easy. This is common in application code. Use thiserror if you are a library that wants to design your own dedicated error type(s) so that on failures the caller gets exactly the information that you choose.

[noted in the anyhow repo]: https://github.com/dtolnay/anyhow#comparison-to-thiserror

#### Benefit

Adding structured errors to the `wasmtime` API will allow for programmatic users to define logic to react to precise error
conditions, without resorting to parsing error strings. A specific use-case that I would benefit from having is being able to recognize when a module is missing a function export.

#### Implementation

It would be great to have one top-level `wasmtime::Error` error enum that divided the error space into discrete variants. The `thiserror` macro could be used to assist in generating the `Display` implementation for this type.

A brief search through the `crates/wasmtime/` directory shows the following number of hits for `bail!` and `anyhow!`:

- `src/types/matching.rs`:
- bail!: 24
- `src/module/serialization.rs`:
- bail!: 17
- anyhow!: 3
- `src/externals.rs`:
- bail! 9
- anyhow!: 2
- `src/linker.rs`:
- bail! 8
- anyhow!: 1
- `src/config.rs`:
- bail!: 5
- anyhow!: 1
- `src/instance.rs`:
- bail! 5
- anyhow!: 1
- `src/values.rs`:
- bail!: 4
- `src/func/typed.rs`:
- bail!: 3
- `src/func.rs`:
- bail!: 3
- `src/store.rs`:
- bail!: 2
- `src/trampoline/memory.rs`:
- anyhow!: 1

This amounts to 83 `bail!` calls and 9 `anyhow!` calls, and does not account for errors that may be bubbled-up from internal or third-party crates using `?`. This is most probably too many instances to create unique variants for all of them, but if we take a closer look at some of these error instances, it becomes clear that we can probably bundle many of them into reusable variants. For example, there are the following `bail!`s:

```
src/types/matching.rs
244: _ => bail!("expected global, but found {}", actual_desc),
248: _ => bail!("expected table, but found {}", actual_desc),
252: _ => bail!("expected memory, but found {}", actual_desc),
263: _ => bail!("expected function, but found {}", actual_desc),
273: _ => bail!("expected instance, but found {}", actual_desc),
299: _ => bail!("expected module, but found {}", actual_desc),
310: _ => bail!("expected global, but found {}", actual.desc()),
314: _ => bail!("expected table, but found {}", actual.desc()),
318: _ => bail!("expected memory, but found {}", actual.desc()),
322: _ => bail!("expected func, but found {}", actual.desc()),
326: _ => bail!("expected instance, but found {}", actual.desc()),
330: _ => bail!("expected module, but found {}", actual.desc()),
342: _ => bail!("expected {}, but found func", entity_desc(expected)),
359: _ => bail!("expected {}, but found instance", entity_desc(expected)),
```

These could all be represented by one variant, such as `Error::UnexpectedEntityType(String)` (name subject to bikeshedding). There are probably other such groupings that can whittle down the number of error variants that would be necessary.

##### API Commitment

Creating a structured error type would expand the public API of the `wasmtime` crate, so it may be something that should be done incrementally, or in a future-proof way. One way to do this would be to make the Error enum `#[non_exhaustive]` so that callers must provide a catch-all when examining it. The initial version of the Error enum could expose some high-impact variants that are obviously useful to be able to inspect, and provide some sort of `Other` or `Unknown` case which could store any unclassified errors as an `anyhow::Error` like how things were done before.

#### Alternatives

The most obvious alternative is to simply not adopt structured errors, or to not do so at this point in time. This would incur zero maintenance burden and retain the flexibility of being able to change error messages at any time, but would not provide the benefit to programmatic users described above.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.