Unified Errors, a non-proliferation treaty, and extensible types

I don’t think you can really avoid the heap (or at least some form of indirect allocation) this way…

First of all, if the compiler aggregates all possible error types into one giant enum, that means every error has the size of the largest error type anywhere in the program. As long as there’s some error somewhere that contains a lot of fields or a large field (e.g. an array), a type like Result<u32, MyError>, which today might be two or three words, will take up probably hundreds of bytes. With a naive implementation, that entire region of memory will be copied whenever a value of that type is moved or even returned, even if most of it was uninitialized to start with because there isn’t a big error stored in it. You might be able to optimize it by storing the used size and only copying that portion of the struct, but that logic would have its own overhead, and in any case there’s still the issue of wasting a ton of stack space.

Besides, how would you deal with the ‘chain’ part of error-chain? That is, how do you store both an outer error and the inner error it was caused by? Currently that’s done just by making the inner error a field of the outer error type. Usually there’s no need for recursion – i.e. the inner error type isn’t the same as the outer error type, and it can’t contain it as one of its fields either – so the inner error can be stored by value, and the result is a finitely sized type without heap allocations. But you can’t do that with one universal error type; you’d have to write

pub enum Exn := MyError { cause: Exn }

but that doesn’t work, because the compiler can’t rule out

MyError { cause: MyError { cause: MyError { cause: MyError { [etc…] } } } }

One potential workaround would be to make the universal error type an array of Exns rather than just one; the inner error would be stored in the next array element rather than actually inside the outer error. But there would have to be some (small) arbitrary limit on error count, and the types get even more bloated…

At that point it’s probably considerably more efficient to just use the heap - at least, when it’s available. (But in the sorts of environments where it’s not available, you might not have that big a stack either, in which case huge error types would create a high risk of stack overflow.)

But then you don’t really need a new language feature; you can just use trait objects. Box<Error> is pretty much what you’re asking for, except you can’t match on it – but if Error inherited from Any you could use the downcast methods.

2 Likes