Starting over with errors

Current state

It didn’t take me long to realize I simply hate working with Option type as a replacement of exceptions — instead of simply writing the code I have to babysit each possible failing function and write conditions for each optional value (I am not the only one in pain). So I read again about Java, C#, Haskell, OCaml and Rust (did you notice Rust team introduced subtle flaw in design — in Haskell and OCaml the correct outcome of the function is the right outcome. In Rust it is on the left, so the right answer is the erroneous answer). I can sum this up as follows:

  • C# approach is a dead end because it leads to duplicating functions — dreadful Do and TryDo pattern,
  • Java annotated functions with exceptions that can be thrown and passed was a mistake, it only takes few minutes of writing the code to feel tired. And for unchecked exceptions it shares the same problem as C#,
  • Haskell looked promising until I saw the real code tackling the errors — so partially I would be in camp I want to escape from already (Skila), and partially in heavy function annotating camp which I dislike since my Java experience. Also I am sceptical how smooth this solution is when it comes to inheritance and when you read “Error handling” chapter (from Real World Haskell by Bryan O’Sullivan, John Goerzen, Don Stewart) you might get the impression that something is fishy, if one feature kills another feature and you need to jump extra hoops to use both.

I have wrong design (more in artistic sense) and three approaches which, well, I don’t like.

I could agree with the sentence “if you don’t have anything right to say, or anything that moves the art forward, then you’d better just be completely silent and neutral, as opposed to trying to lay out a framework” (from interview with Anders Hejlsberg) but not in this case — working on and implementing the language which is broken from start is no fun, and has no value at all. I must make a second, third — fourth if necessary — try to find out the solution. After all it has to be somewhere.

Envisioned design

Skila will have exceptions but they will be thrown usually by code injected by compiler. All errors in computations should be signalled by:

  • optional value — if it is likely that user will handle the error. It is counterpart of function that requires its outcome to be read. As an example consider String.indexOf — not finding character in string could lead to an error, but it is rather not an error by itself,
  • Error type — here the situation is reversed, it is more likely that the “client” would treat an error like a serious computation failure and pass it for handling to some upper function, it is counterpart of the function that allows its outcome to be ignored. Every unhandled error is automatically converted to exception. Error type will be subtype of any other type, so returning an Error instead of “real” value does not need to (and cannot?) be annotated in function signature. In other words every function can fail.

You can convert optional value to exception (this is already done), you can convert error to exception or to optional value. The beauty I foresee is present in several aspects:

  • it is transparent to inheritance,
  • it should not clash with any other feature,
  • single function will be enough,
  • the flow of the errors is layered,
  • no extra code necessary — you want to cope with errors, you do it, you don‘t want to — simply behave naturally (as with exceptions).

What is layered flow of errors? By default, the function you talk directly to could give you an error, but all third party functions involved in computation can result in exception (because every unhandled error becomes an exception). You could depict such propagation in default flow:

deep function ⟶ nearby function ⟶ your function
  (error)      |                  |
               |     (error)      |
               |   (exception)    |
               |                  |   (exception)

In the first line you see deep function returns an error. The error is passed to the nearby function (line 2), since it is incoming error and it is unhandled it becomes an exception (line 3), it is then “passed” to your function. In more real life perspective, when you call for example dict[key] (nearby function) for non existing key you will get error and you can handle the error right away. But if the failure is caused because there is a some logging (deep function) involved, and that logging failed, you will get an exception, not an error.

Again, this is the default flow — every function can handle the values it gets and convert them to whatever necessary.

Problems

Entire mechanism is very similar to passing null in C# — in Skila it will be done on purpose, as a bait. But there is difference — in C# you can pass null without triggering exception, in Skila even passing has to be checked. This is for easier finding the source of the error and also to consistently process all the functions including those which work for their side effects (all Void functions are prime examples). So performance can be a victim here.

Tagged , , , , , ,

10 thoughts on “Starting over with errors

  1. mortoray says:

    I like your idea of treating errors as a kind of optional value. This is the approach I want to try with Leaf as well. You can basically treat the return of a function as a value, so clean syntax, but if it happens to be an error, and you attempt to use it, the error will propagate. Explicit error handling can then then done in the traditional try/catch setup, or simply as trapping this special optional and using if statements.

    I also don’t have checked/unchecked exceptions. Everything can throw an exception since I consider that the default state of fucntions. It’s a rare exception to have functions which atually can’t produce errors. I have only three levels of errors as well, trivial (can be undone), recoverable (need work to undo), and critical (no chance of recovery). All additional exception information is in the form of tags: I found this model from boost_exception in C++ to be an excellent model. You can add arbitrary information without needing wrappers.

  2. macias says:

    Hmm, since you didn‘t like the Rust approach (correct?) I understand that you have implicit optional value, not explicit one (as it is usually implemented) — i.e. conversion from optional type T to type T would have to be implicit. I am not sure if this is what you have in mind, but after your comment I wrote it down as another approach :-).

    I will try out the model I described above first because (at least for now) I don’t see a way how to comfortably write and use functions like assert with implicit option. In my case assert can produce Error directly, compiler will check if it was created at the place when assert was called. If it was created current function is aborted and Error is returned but now in disguise (as some type T). The rest is as I described in my post.

    With implicit option what would be the outcome of assert? Optional Void? How it will be converted to optional T? And how compiler will tell such conversion should be checked (for regular use) or unchecked (for assert client)?

    As for exceptions I will also not add such distinction but probably even without any levels — that’s because I am afraid my imagination is too limited to say today whether X is critical or not. In year somebody could come with some idea that critical exception could be nicely handled as recoverable. Initially I rather have in mind just an Exception (single type), possibly chained, possibly with message, and of course with stack trace. I will look into those tags, thank you for the hint.

    • mortoray says:

      In Leaf the error code is an implicit part of all functions, so it never shows up in the function signature (there’s a marking to say when a function cannot generate an error, mainly for library compatibility). The no return function, such as `()->()` is enough to indicate it can produce an error: it’s the default. The return value specification is not overloaded to include error values.

      By default errors are automatically propagated. Only if you attempt to handle them does this automatic propagation get interrupted. I’m not clear on the syntax yet, but I’m thinking something like `trap expr…` which wraps the result in an error type.

      My current optional type already deals with implicit assignment and has a lot of syntax to work with it. I’m going to reuse that for the error types. Though it creates a bit of a problem if you wrap an optional type in the error type (major syntax ambiguity).

      Critical errors for me are extremely rare. They indicate the abstract machine has broken somehow or cannot be reasonably assumed to continue (like an allocation of a small object fails, or a critical OS handle has gone missing).

      • macias says:

        So it seems my “error value” is like your optional value, with such difference that in my case errors don’t propagate — it would be easier if they were, but I opted against to differentiate between error “key was not found in a dictionary” and exception (for the same call) “the key was not found because we could not connect to the database because the configuration was incorrect, because there was parsing error, because we couldn‘t convert ‘S’ to integer”.

        I am eager to see and compare our solutions once they will be implemented.

        • mortoray says:

          I’m not going to distinguish between those types of errors. It’s been my expectiern that it’s just impossible to make a line between what is an “exception” error and what is a “common” error. I’ve also found that the caller rarely cares about the difference. If I’m looking for something in a DB either I find it or I don’t, it makes little difference as to why.

          But I won’t know how this works until I get further. I’m just building my standard library basics now (loading/saving unicode files).

          • macias says:

            Clarification — it is not difference between common and uncommon error, it is difference between relevance and lack of it. I ask — give me “12x” as integer. Relevant answer is — “it is not a number”, irrelevant is “you call could not be logged in the database”. I ask — give me the value for “style” attribute of this “div”. Relevant answer is — “sorry, there is no such attribute”. Irrelevant one — ”couldn’t connect to the host”. And so on.

          • mortoray says:

            I actualyl don’t consider relevance to be a good metric for deciding how to report an error. If I’m asking for a record in the DB, how I handle the error at the call point will not change depending on what type of error it is. In all cases, invalid parse, connection, or otherwise, I have to undo my current state and propagate. The type of error is only relevant to something much higher in the call stack that can present messages to the user.

          • macias says:

            As it appears wording is crucial :-). For me “undo my current state and propagate” is not handling the error, it is just propagating with or without recovery (I am not touching this because I have nothing to improve here over C#, Go or D). Handling it would be (as I understand it) translate it to some value (or extra logic) which makes sense for given function — and I focus on exactly this. Let me get back to the example with HTML and CSS. When I write web scrapper and I ask “div” for value of the attribute “style” it is pretty common I will get error (no “style” attribute), but I will handle it — not recover, not propagate, I simply look for another “div” and I will continue the search. To recap, I would like to make a distinction between (when asking for values) – there was required value, there was no required value (we checked), and we crashed while checking. There should be enough tools in the language to make smooth transition between those cases.

          • mortoray says:

            If you are looking for a `div` though, expecting not to find one, then you should be using a function that optionally returns one. This in my mind isn’t an error at all. The function is expected to not be able to find one. If, on the other hand, I’m expecting to find something, it would be an error if I can’t.

            These are two different functions with different signatures `(:string)->(:node)` and `(:string)->(:optional node)` in Leaf syntax. The idea that a node may not exist is encoded in the return value, but it has nothing to do with an error condition. I’m trying to avoid this overloaded/mixed use of errors. I think it comes up in languages that allow `null` to be returned for objects: the signature thus isn’t clear if that’s an expected condition.

          • macias says:

            “The idea that a node may not exist is encoded in the return value, but it has nothing to do with an error condition”. And this is the problem I try to solve — it depends. I hope you agree that if you have 10 elements array and you write “array[17]” it should be an error. OK, but what about the dictionaries “dict[17]”? Error? In such case you just contradicted yourself. Value? Little inconsistency.

            In Skila it will be error in both cases — I could write “dict[17]” all the time and get errors (default) or write “dict[17] ??” to convert the error to optional value. The dictionary I use here is general purpose dictionary, nothing is tailored for processing HTML/CSS, simply in this context, while traversing nodes it will be more useful to handle the errors (not-found). But it is client who decides. In C# you have such methods as “Parse” and “TryParse”, “Get” and “TryGet”, and so on. The author of the library has to provide 2 functions each time, because the client might use this or that. In Skila the author will provide only one function, but its nature can be changed when using it.

            Of course it is possible to write the function which returns optional value as well, like “String.indexOf”, and convert it to error if needed.

Leave a reply to macias Cancel reply