Exceptions

Exceptions are MiniD's error handling mechanism. Honestly, if you haven't used or at least heard of exceptions by now - what rock have you been under for the past 20 years? ;)

What are exceptions?

If you already know what exceptions are, feel free to skip to the next section.

If you somehow haven't heard of exceptions, they are a way of error handling that uses out-of-band signaling. What does that mean? Well to contrast, in C, normally one uses in-band signaling. You call a function and it gives you a return value. If the return value is in some range of valid values, the function must have succeeded; otherwise, it failed. An example is the C malloc function: it returns the address of a newly-allocated block of memory if it succeeded, and NULL if it failed.

There are several problems with using in-band signaling for indicating return values.

  • You have to manually check the return value of every function to see if an error occurred, which is tedious, hides the actual algorithms, and is easy to mess up.
  • Cleaning up after an error occurs is equally tedious, especially when there are multiple possible points of error in one function.
  • It's hard to embed any kind of useful information about the error or why it occurred in a single value or in a small enumeration of values.
  • There is no checking done by the language that you are actually using the return value correctly. That is, if malloc returns NULL, you can still use that pointer, even though it signifies an error condition.
  • What if your function's return value is defined across the entire range of the output type, such as division? Then in-band signaling is undesirable since it must overlap with another value that otherwise would not be an error.

Exceptions provide a fairly neat solution to all of these problems. It's of course not a panacea, but it's far better than error code checking. Exceptions are out-of-band in that there is a mechanism entirely dedicated to creating and handling exceptions that is separated from the function calling mechanism. Exceptions allow you to write much cleaner, more direct code, and doesn't require you to design your function return values around error conditions.

Exceptions are closely related to a contradiction in a formal proof, or to a "crash" in some state machine formalizations. When an exception is thrown, the code that throws it is saying "I don't know how to proceed; something is wrong and there is no valid course of action."

What happens after that, then? How is the error handled? What you can do is set up error-handling blocks called try-catch blocks. The "try" code is tried, and if any exception occurs within the try block, execution jumps immediately to the catch block. The catch block can then handle the error or throw the exception again (or a new one). If no exception occurs in the try block, the catch block is skipped entirely. These try blocks can be nested, including across function calls. When an exception occurs, only the nearest catch block is jumped to.

In addition to the try and catch blocks, there's also the "finally" block. This is code that is always executed, regardless of whether an exception occurred or not. If an exception occurs in the try block and is not caught, any finally blocks are executed from innermost to outermost until either the exception is caught or it leaves the program. If the try block completes successfully, on the other hand, the finally block is executed immediately after it completes (and then execution continues with the statement after the finally block).

What if you don't handle errors? This is one of the nice things about exceptions: you don't have to care. If you don't handle errors, your program might be a bit of a pain to use, but it will never be in an invalid state. That is, if your program expects the user to type in a number and instead types in "fork", the string-to-number conversion will fail, an exception will be thrown, and since it isn't handled, it will leave the program and the program will terminate. That's much better than forgetting to check the stupid "errno" variable after strtol fails and then using its erroneous return value of 0 which might mean, to your program, "I want to resize my boot partition to 0 MB."

How do you use them in MiniD?

For those of you who already know what exceptions are, you probably won't need too much help on this section either. The syntax is practically identical to D, Java, ECMAScript and C#.

To set up an error-handling block, you use the following syntax:

try
{
	// code that might throw an exception here
}
catch(e)
{
	// e is the exception that was caught
	// code to handle the exception here
}

As explained before, the "try" block sets up a "safe area" in which exceptions may occur, and if they do, execution jumps to the "catch" block. If an exception causes execution to jump to the catch block, the exception object itself is placed into a new local variable whose name you put in the parentheses after "catch". This variable is local to the "catch" block and like any other local, can't be named the same as an existing local.

How do you throw an exception? I've been dropping hints all along: you throw an exception with - surprise! - "throw".

try
	throw "ACK!"
catch(e)
	writeln(e)

The "throw" statement just takes a single value of any type and throws it as an exception. Here we're throwing a string.

So what happens in this code? The try block begins, and the first thing it does is it throws an exception. So it looks for the innermost catch block, which happens to be right below it, and jumps to it. The exception is caught and its value is placed into the 'e' variable in the catch block. The catch block handles the error by just printing it out.

Let's nest some try blocks:

try
{
	try
	{
		writeln("inner try")
		throw "ACK!"
	}
	catch(e)
	{
		writeln("inner catch: ", e)
		throw e // rethrowing it
	}

	writeln("I'm never executed!") // never prints
}
catch(e)
	writeln("outer catch: ", e)

This is an entirely contrived example, but it shows the flow of the program. The first thing that prints out is "inner try". Then the exception is thrown. The innermost catch block is jumped to, and it prints "inner catch: ACK!". Then it rethrows the exception that it just caught. This is just fine, and in some cases, exactly what you need to have happen. Now where does execution go? The second throw is in a nested catch block, but by now, the nested try block has ended. So it jumps to the outer catch block, which prints "outer catch: ACK!".

"I'm never executed!" is never printed out, since execution jumps right from the second throw to the outer catch.

Remember that nesting try blocks works across function calls as well.

The next example shows that finally blocks are called whenever a try block is left, either successfully or by an exception being thrown:

function doSomething(x)
{
	try
	{
		if(x >= 0 && x < 10)
			return true
		else
			return false
	}
	finally
		writeln("exiting doSomething")
}

writeln(doSomething(5)) // prints "exiting doSomething" followed by "true"
writeln(doSomething(13)) // prints "exiting doSomething" followed by "false"
writeln(doSomething('x')) // prints "exiting doSomething" followed by an error report

The last line will throw an error inside doSomething since it tried to compare a character to an int, but even so, it still prints "doSomething".

But there's no throw here. How did the "finally" block still work?

What about other errors?

What about errors like the above, where we tried to compare a char to an int? They're exceptions too. All the errors thrown by the compiler, interpreter, and standard libraries are just strings which are thrown as exceptions, which means you can catch and handle them just as you would any of your own exceptions.

Multiple Types of Exceptions

The language does not enforce any kind of exception classification or hierarchy of exception types, but you can implement your own pretty easily. All you have to do is have an exception base class from which all your exception types inherit. Then, when you catch an exception, see if it's an instance of the exception type you want to catch.

class Exception
{
	msg
	this(msg: string) :msg = msg
	function toString() = nameOf(this.super) ~ ": " ~ :msg
}

class RangeException : Exception { /* ... */ }
class IndexException : Exception { /* ... */ }
class IOException : Exception    { /* ... */ }

try
{
	// icky stuff here.
}
catch(e)
{
	if(e as RangeException)
		writeln("Caught range exception!")
	else if(e as IndexException)
		writeln("Caught index exception!")
	else if(e as Exception)
	{
		writeln("Onoz, I don't know what kind of exception it is.")
		throw e
	}
	else
	{
		// not an Exception instance, some other kind of error.
	}
}

If you had a ton of exception types to handle, you could even switch on the exception instance's class:

if(e as Exception)
{
	switch(e.super)
	{
		case RangeException: ...
		case IndexException: ...
		case IOException: ...
		case ArgumentException: ...
		case OMGWTFException: ...
		default: throw e
	}
}

Tracebacks

Something that's often very helpful in finding out what caused an error is if you can find out where it came from. Knowing the location of the statement that caused the error is nice; but knowing the entire call stack from where the error occurred is often much more helpful. This is called the traceback.

Getting the traceback after an exception is thrown is very easy: you just use the "thread.traceback" function, which returns a string where each line is one function on the call stack. For example:

module example

function fork()
	throw "Fail!"
	
function knife()
	fork()
	
function spoon()
	knife()
	
try
	spoon()
catch(e)
{
	writeln("caught: ", e)
	writeln(thread.traceback())
}

This shows the following output:

caught: Fail!
Traceback: example.fork(4)
        at example.knife(7)
        at example.spoon(10)

Notice that the traceback shows, in reverse order from where the exception was thrown to where it was caught, the function and line number of the function calls that were on the call stack.

Up Next

That's really all there is to say about exceptions. Next are modules.