Open lukewagner opened 3 months ago
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 error
s, though? Sounds like you're only one step away from describing a general purpose any
type (as found in e.g. Rust).
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?).
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?
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).
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 ani32
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 anerror
(via some newerror.new
canon built-in) and can be conditionally extracted by wasm (via some other newcanon.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 atcanon.new
and could be repeatedly extended (by creating newerror
values from the oldererror
values) with additional context when propagating anerror
.error
values could be logged directly (via theiri32
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 uncaughtError
objects)Some potential benefits of having
error
be built-in include:error
to the languages' idiomatic error-with-context constructs (e.g., a JSError
object or Rustanyhow::Error
)error
is created" runtime option to help debug cases where errors are swallowed or handled incorrectly (similar to first-chance exceptions).error
and all associated payload-accessor functions (likehttp-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
andoutput-stream
want to use a single wasi-io-defined, payload-agnosticerror
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-levelerror
type (which you get when reading a stream and an error occurs). So if nothing else, I'd like to consider addingerror
in a Preview 3 timeframe, but if anyone was keen to work on this earlier, we could work on justerror
earlier.A few high-level open questions:
error
" canon built-in mentioned above, we might want it to just be a general structured logging function (that happens to takeerror
s). 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 anerror
" built-in (initially).error<P>
andstream<T,P>
whereP
was the error payload type, butP
was ignorable via subtyping rules sayingforall P. error<P> <: error
and similarlyforall 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-httpresource 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.