component-model icon indicating copy to clipboard operation
component-model copied to clipboard

Consider adding an `error` type

Open lukewagner opened this issue 6 months ago • 4 comments

I think it would be beneficial to add a built-in error type to the Component Model and WIT to serve as the go-to type to use when you want to propagate rich error information for the benefit of debugging. With this type, result<T, error> would become the common return type of fallible functions.

Just as a high-level sketch to paint a picture:

  • error could have (immutable) value semantics but be represented in core wasm as an i32 index into an error table managed by the runtime (like resources/handles), allowing it to be efficiently propagated between components without the usual linear-memory copy of normal values.
  • error values could contain a boxed heterogeneous value payload that is supplied by wasm when creating an error (via some new error.new canon built-in) and can be conditionally extracted by wasm (via some other new canon.payload built-in) if the payload type dynamically matches.
  • error values could also contain context information (incl. a callstack) that would be automatically filled in by the host at canon.new and could be repeatedly extended (by creating new error values from the older error values) with additional context when propagating an error.
  • error values could be logged directly (via their i32 index passed to another new canon built-in) such that the host could easily do a nice job rendering the context for the developer (analogous to how browser consoles nicely render the stacks of uncaught Error objects)

Some potential benefits of having error be built-in include:

  • Bindings generators would have the additional semantic information to bind error to the languages' idiomatic error-with-context constructs (e.g., a JS Error object or Rust anyhow::Error)
  • The amount of context captured could be configured in the wasm runtime by the developer (independently of the code being executed), allowing full expensive context to be captured when useful (debugging) and cheap or even zero context to be captured in high-volume scenarios where performance or cost are being optimized.
  • Hosts could offer a "log every time an error is created" runtime option to help debug cases where errors are swallowed or handled incorrectly (similar to first-chance exceptions).
  • wasi-io error and all associated payload-accessor functions (like http-error-code) could be removed. These are currently a source of anti-modular coupling between the implementations of unrelated WASI interfaces, with the net effect being that if you want to virtualize just one WASI interface that uses wasi-io, you end up being forced to virtualize/wrap them all.

Additionally, for the same reason that wasi-io's input-stream and output-stream want to use a single wasi-io-defined, payload-agnostic error resource type (instead of having each distinct WASI package define its own stream type with its own domain-specific error variant), I think Preview 3 async depends on there being a single C-M-level error type (which you get when reading a stream and an error occurs). So if nothing else, I'd like to consider adding error in a Preview 3 timeframe, but if anyone was keen to work on this earlier, we could work on just error earlier.

A few high-level open questions:

  • Once we have the "log an error" canon built-in mentioned above, we might want it to just be a general structured logging function (that happens to take errors). I think there'd be a lot of benefits to this too, but it does increase the problem scope... but maybe not too much? Alternatively, we could cut the other way and hold off on the "log an error" built-in (initially).
  • We could add a bit more static type information about the error payload via optional generic parameters: error<P> and stream<T,P> where P was the error payload type, but P was ignorable via subtyping rules saying forall P. error<P> <: error and similarly forall P. stream<T,P> <: stream<T> (noting that error payload values are not lost in these subtypings; just the static knowledge of their type). This would provide better static typing in common cases where you're directly consuming the result of, e.g., wasi-http resource body { consume: func() -> result<stream<u8, error-code>>; }. I'm not sure if it's worth the hassle to add this though and we could always add it backwards-compatibly later, so I'm inclined to leave it out (initially).

There's a lot of details left to figure out (and maybe the basic sketch isn't right either), but I thought I'd file this now to collect thoughts and use cases.

lukewagner avatar Aug 21 '24 23:08 lukewagner