I’ve been thinking a lot about error-handling in Magpie, and I wanted to foist
a couple of ideas out there to get some feedback.

Kinds of Errors

When I’m designing something, I tend to start from a few representative
examples, and then I see how my idea for a solution handles those cases.
Error-handling encompasses a pretty wide range of cases, everything from “you
typed the name wrong” to “the machine is on fire”. That space is pretty big,
but I think the following touch the most important corners of it. Let me know
if I missed something:

Programmatic Errors (The Programmer Screwed Up)

When I look at code I wrote, most of the error-handling code in it is for
handling errors in other code. In other words, it’s code contract stuff:
validation code that asserts what arguments should look like and what state
objects should be in when you try to do stuff with them.

When possible, it’s good to squash those bugs statically so you don’t need
code to handle them at all. Magpie does that with null reference bugs, but
there are others that are trickier: things like out of bounds array access, or
attempting to cast a variable to the wrong type.

In C and C++, these errors are usually handled using assert() or similar
mechanisms. Java and C# each have a set of standard exception types that are
thrown: InvalidOperationException, IllegalArgumentException, etc.

Checks for these errors are very common, so they should be lightweight in both
in code and in CPU cycles. At the same time, we don’t generally handle these
errors, in the sense of recovering at runtime. Instead, we just want to
notice the error and scream and shout to the programmer to handle it by
fixing the code that causes the error in the first place. Giving diagnostic
information like a stack trace is good.

Runtime Errors (The Real World Isn’t Perfect)

I lump into this fuzzy category errors that can occur at runtime and either
can’t be programmatically prevented, or where doing so is as expensive as
performing the operation itself. Some examples:

Working with files or the network.

Parsing, formatting, or manipulating data.

These are the kinds of things we typically think of when we think of “errors”.
We want to notice when they’ve happened, and we will very likely need to write
code to try to recover from them gracefully at runtime.

What this category doesn’t imply is any sort of frequency. Whether or not
a given error is common is entirely dependent on the application. A blog
engine probably considers text parsing a common source of errors. A game app
that just loads a single config file with a known format can safely assume
they won’t happen.

This means we want to have some flexibility regarding how errors in this
category are handled. If we force a certain strategy by assuming that some
errors are common and some are rare, then we’ll shaft users that aren’t like
us.

Catastrophic Errors (All Hell Breaks Loose)

The last category is errors that are so deep that we probably can’t handle
them. These are errors are distinguished by the fact that they interfere with
our ability to execute further code: things like stack overflows or running
out of memory.

Handling Errors

There are a bunch of different strategies languages have tried over the
years to cope with the inevitable fallibility of mankind. The two I’m most
interested for Magpie are return codes (using unions) and exceptions.
Those seem to be the workhorses for languages in wide use. (I’m interested in
other ideas, but for this post, I just want to look at those two.)

Now the question is how can we use those two features to deal with the
different categories of errors I listed up there?

Handling Programmatic Errors

These are probably the easiest to solve (from the language design perspective)
because there’s little to do: we don’t plan to handle them in most cases, just
notice them. I’m comfortable with the Java and C# model of “throw an exception
that isn’t expected to be caught”. Aborting with a stack dump is equally
effective, and does the same thing in practice.

For example, a method for accessing an item in a collection could look like:

defCollectiongetItemAt(indexInt->Item)ifindex<0orindex>countthenOutOfBoundsErrorthrow()// do stuff with index...end

That is a bit tedious, though. I’d likely refactor that into a separate
function, like:

defIntcheckBounds(countInt->)ifthis<0orthis>countthenOutOfBoundsErrorthrow()enddefCollectiongetItemAt(indexInt->Item)indexcheckBounds(count)// do stuff with index...end

Not exactly rocket science, but I think it gets the job done. Let’s skip
runtime errors and move on to the other easy one:

Handling Catastrophic Errors

Catastrophic errors are exceptional in the sense that we’ll rarely be handling
them, so exceptions are a good fit here too. In fact, most of these exceptions
wouldn’t even be thrown from Magpie code— they’d bubble up from the bowels of
the interpreter itself.

On the off chance that you do want to catch one, you can use a regular catch
block:

try// allocate a huge array...catch(errOutOfMemoryError)end

Familiar territory. If you’ve used exceptions a lot, you’ve noticed one
annoying thing with them is that they’re syntactically cumbersome: you have to
create this try block and push everything over a level of indentation. To
try to simplify that, I’m batting around an idea that might be clever, or
might just be really dumb: treat every block as a try block.

The basic idea is that any block can have catch clauses at the end of it,
and having them implicitly makes it a try block. That should get code like:

I’ll have to try it out to see if it causes any problems in the grammar, but
my hope is it will work OK. I’m curious to see if just making exceptions a
little more terse like this will make them more palatable to people who
dislike them. If you happen to have an opinion, I’d like to hear it.

Handling Runtime Errors

Finally, the biggest class of errors. The trick with these is that there’s no
easy way to bucket them into “common” and “rare”. If we could, we could just
say “use exceptions for the rare ones and return codes for the common ones”.
Instead, we’ll need to support both.

Here’s my plan. For our example, we’ll consider a simple one: parsing. Let’s
say we have a function to parse strings to booleans:

defparseBool(textString->Bool)matchtextcase"true"thentruecase"false"thenfalseelse// ??? what do we do here?endend

This can be called like:

varb=parseBool("true")

Of course, the question is what happens if it fails? Since this may be common,
we want it to be easy to handle the failure case. Unions are a good fit for
that. We’ll change the function to:

Now it will return a boolean value if the parse succeeds, or the special
nothing value if it fails. Note that this is not like just returning
null: the return type of parseBool is different now. That means you can’t
do this anymore:

varb=parseBool("true")varnotB=bnot

The not method is a method on booleans, and b isnt’ a boolean, it’s a
Bool | Nothing. To treat it like a boolean, you first have to check its
type. The canonical way to do that in Magpie is using let:

letb=parseBool("true")// in here, b is a BoolvarnotB=bnot// this is fineelse// parse failed...end

This is great for cases where parsing is likely to fail. It makes sure you
always handle the common failure case by giving you a type-check error before
the program is run if you don’t check for success first.

But what if parsing rarely fails in your program? Do you really want to have
to do a cumbersome let block everywhere you call parseBool just because
that fails all the time in some other program?

In your case, failing to parse is exceptional, so it should throw an
exception. That way, you can ignore the cases that aren’t relevant to your
problem. I think we can handle that too.

We’ll just add a simple method to Object that tests to see if its of an
expected type. If not, it will throw, otherwise it will return itself, but
statically-typed to the expected type. Like so:

defObjectexpecting[T]letcast=thisas[T]thencastelseUnexpectedTypeErrorthrow("Expected type "+T+" but was "+thistype,this)end

Now, if we have a function that returns a union containing an error, we can
translate that to an exception instead like this:

// doesn't expect a parse errorvarb=parseBool("true")expecting[Bool]varnotB=bnot// ok, since b is a Bool

Using this, almost all functions that can have runtime errors will be
implemented by returning a union of success and an error code, like:

Of course, none of this is any real innovation. My goal here is just to round
off some of the sharp corners of exceptions and return codes and see if I can
make the process of dealing with errors a bit more flexible and readable.
Thoughts?