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

Consider adding an error type #389

Open
lukewagner opened this issue Aug 21, 2024 · 4 comments
Open

Consider adding an error type #389

lukewagner opened this issue Aug 21, 2024 · 4 comments

Comments

@lukewagner
Copy link
Member

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.

@badeend
Copy link
Contributor

badeend commented Aug 23, 2024

All in all: this seems sensible to me. The concept of "errors" are definitely ubiquitous enough to warrant special casing in the component model.


With this type, result<T, error> would become the common return type of fallible functions.

If you were to translate this to today's WIT syntax, does this mean that result<A> effectively becomes result<A, error>, and result<A, B> becomes result<A, error<B>>?


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.

Makes sense. Does the payload field behavior need to be special-cased for errors, though? Sounds like you're only one step away from describing a general purpose any type (as found in e.g. Rust).

@lukewagner
Copy link
Member Author

lukewagner commented Aug 26, 2024

If you were to translate this to today's WIT syntax, does this mean that result effectively becomes result<A, error>, and result<A, B> becomes result<A, error<B>>?

I'm not proposing to change the meaning of result<A> -- that would continue to mean "the error case contains no payload" -- but if what you're asking is whether anyone would ever continue to use result<A> (vs. result<A, error>), I think there may be cases where result<A> still makes sense: when the failure case is trivial or uninteresting, error might feel like overkill. (If we find that in practice we almost always want the error case of result to have an error and thus we want to change result<T,E?> to mean variant { ok(T), %error(error<E?>) }, then we'd need to find a backwards-compatible plan to transition Preview 2 code, which will be tricky, so I'd like to wait and see if that is indeed the case.)

Does the payload field behavior need to be special-cased for errors, though? Sounds like you're only one step away from describing a general purpose any type (as found in e.g. Rust).

I think error payloads can only include pure (stateless, identity-less) values, which rules out handles, futures, streams and buffers and thus it wouldn't be a true "any". Adding a general-purpose any type would have a number of subtle and tricky design questions so I'd ideally like to hold off on doing that until necessary (which, after we add WIT templates, may be never?).

@badeend
Copy link
Contributor

badeend commented Aug 26, 2024

I think error payloads can only include pure (stateless, identity-less) values, which rules out handles, futures, streams and buffers

Okay👍. Out of curiosity: why's that?

@lukewagner
Copy link
Member Author

If error payloads can contain handles or other impure values, then we'd have to worry about all the lifetime/ownership rules for error, which would be even harder than normal since the payload type has been erased. OTOH, if error can only contain pure values, all these lifetime issues go away and the implementation can just be simple ref-counting (noting that pure values and contexts are acyclic).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants