Tag Archives: exceptions

Introducing nearby exceptions

While working on this post, I discovered that Swift uses already very similar approach of handling the errors.

The implementation is cheapest I could think of, I am aware of the problems with backend, but enough with excuses — it works, so I will be able to play with it and test if the model is useful or not.

To keep things short let’s start with C# approach which is used in Skila as well:

Errors as exceptions

You access an array with index out of its boundary. You divide by zero. In cases like those you throw an exception to indicate the error in computation.

Errors as values

Your common sense, practice, intuition, tells you that for some computations throwing an exception would be too harsh, so instead you return magic value instead to indicate “not a value” value. Probably the best example is String.indexOf — for such purpose Skila has type Option<T> and special value null (it is not null you know from C# — it is like Scala none or null in Swift or Kotlin).

Ok, that’s it.

Wait, we missed the magic ingredient which makes exceptions fun to work with! Consider accessing the dictionary — to provide similar API as with arrays, when the key is not found the exception is thrown, it happens in C# and Skila as well. But it would be useful to be able to try to fetch the value — in C# you have to add another function to the class (TryGetValue in this case). In Skila you convert one mode (error as exception) to the other one (error as value):

let dict = {String:Int}();
let val ?Int = dict["hi"] $;  

Let’s focus on the second line — when we access the dictionary (it is empty) the exception is thrown from the indexer getter. That exception in Skila is special — it is nearby exception (because it is thrown in nearby, or next if you like, function we just called). Then there is dollar — $ — character which works as an exception trap. Only nearby exceptions can be trapped (the others go through) — and if this happens, they are converted to null value.

You don’t have to write duplicated code as in C#, you simply convert errors as you like — go from null to exception, or from exception to null. Both conversions are expressions and they are pretty accurate:

def thrower() String => throw Exception();

let dict = {String:Int}();
let val ?Int = dict[thrower()] $;  

Can you tell what will happen? The program will crash — that is because we trapped exception coming from dictionary indexer, not a thrower.

Why not trap all exceptions? Because I would like to distinguish between three outcomes in total — we looked and we found the key, we looked and we didn’t find the key (i.e. we are sure the key is not there), we crashed while looking (because, say, our comparison function is buggy), so we cannot tell if the key exists or not.

Maybe it is an overkill but after a day of playing with exception trap I felt I miss one more feature — consider reading an ini file and then adding key and value pair to properties dictionary:

properties!add(key,value);

When the key already exists we will get exception with a message and stack trace. However we won’t get plenty of information about the context of the problem, like the filename or line number. Sure, we can debug, but we might add those information right away:

properties!add(key,value) && "File ${filename}, line ${line}.";

The mechanism is old as exceptions — chaining. You can chain an exception or a string (Skila will create basic Exception for you). Since the new exception is created it is equivalent of explicit throw, i.e. for the outer function such chained exception will be a nearby exception.

And to just describe all the tools required for the job — there has to be a way to close the distance of the exception, otherwise assert and fail would be useless:

def [key K] V
set:
  •••
  fail("Key not found.");
end

Your code calls dictionary indexer, dictionary indexer calls fail, fail calls assert, assert throws an exception. So for sure such far placed throw will not be considered as “nearby” from your code perspective. Unless we tamper with the exception distance counter:

def assert(•••) Void
  •••
  throw Exception(•••) &&>>;
end

def fail(•••) Void
  assert(•••) &&>>;
end

This cryptic code &&>> tells compiler to pass an exception further. Effectively when you write assert or fail the exception which is thrown thinks it originated from where those functions were called.

Tagged , ,

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 , , , , , ,