Errors

I realized that it been a while since I had written a post. I suppose that is good, because the reason is that things in the software itself have been coming together.

After a lot of back and forth and trying various things to see how they “felt”, I finally made a decision on errors. The question was in what way should errors be signaled from the implementation of a system call to the client (WASM) side or from the implementation of a service.

I’ve decided that the go functions that return results to be consumed by the WASM side, should return errors “out of band”. In this case, out of band means that the errors are returned separately from the response object. So a function that is implementing some part of a system call might look like this:

func fooImplementation(req *FooRequest, resp *FooResponse) (lib.Id, string)

So if there are no errors the return values are nil and "". The happy path will consume the results of the function via the resp parameter.

Why lib.id and string?

The reason for using lib.Id and string as the return values is that this somewhat simulates the “error” object in go, but in a language independent way. Normally, the lib.Id return value is an error id, such KernelErrorId or QueueErrorId. The string should be more details about the error and is intended to be read by humans. This setup makes it easy to write tests, becasue the automated tests can just check the lib.Id and ignore the string with the error details.

Crossing the boundary

I said that the go implementations of services and the kernel would return errors out of band. Well, that’s really not possible when we have to travers the boundary between go and WASM-land.

For example, here is the structure used when we cross the boundary. The same structure is used for all the calls, just the content of the in and out values (really the Request and Response objects) change.

type SinglePayload struct {
	InPtr        int64
	InLen        int64
	OutPtr       int64
	OutLen       int64
	ErrPtr       [2]int64
	ErrDetailLen int64
	ErrDetail    int64
}

This is the structure that defines how a call is made from WASM-land to the kernel or a service implemented on the host system. That structure has several pointers in it, like InPtr and OutPtr, and these are set up by the caller on the WASM side. The caller may have any amount of shenanigans going on with memory in his address space, such as multiple stacks and a running garbage collector. So, the client has to allocate the memory for both the in and out data (pointed to by the InPtr and OutPtr) and tell us how much memory is used (input) or available for writing (output) in the InLen and OutLen.

The last few fields are for the error code and error detail if any. Again, all the memory pointed to be ErrorDetail has to be allocated by the caller and the caller informs the kernel about the space allocated through the Len field. Strings are encoded as a pointer and a length field.

In the future, I hope to take this error handling stuff further and allow the client side libraries to accept parameters, return values, and signal errors in a way that is natural for their respective languages. For example, python should probably use raise to signal errors, not return them as part of the return from a function.