Error Handling

MiniD uses exceptions to handle errors. So does D. Wouldn't it be nice if the two systems worked together? Well they do. In fact whenever you throw an exception in MiniD, a "real" D exception is thrown. The MiniD interpreter uses D exception handling to handle errors. What this means is that you can throw an exception in either native code or MiniD code, and then catch it in either native code or MiniD code.

As far as the MiniD side of things, exception handling is pretty much automatic. But on the native side, you'll have to do just a little extra work. This is because when you throw a MiniD exception, the interpreter sets some internal flags. When the exception is caught, those flags need to be changed. When MiniD code catches an exception, the interpreter does this automatically; but when native code catches it, it must inform the interpreter that it has done so, or else strange behavior will result.

If you want to catch a MiniD exception in native code, you just set up a try-catch block. The exception type you'll want to be catching is MDException, defined in minid.types. This exception class doesn't hold much more than a string representation of the exception that was thrown. But once you catch the exception in D, you have to inform the interpreter that you have caught it by using the "catchException" function. Consider the following:

module example;

import tango.io.Stdout;

import minid.api;

void main()
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	try
		runString(t, "local x = 3; x += 'x'"); // obviously illegal
	catch(MDException e)
	{
		auto mdEx = catchException(t);
		
		// e is the D exception object, and mdEx is the stack index of the MiniD exception object.
		
		Stdout.formatln("Error: {}", e);
		
		// pop the exception object off
		pop(t);
	}
}

So in the catch block, we signal to the interpreter that we have caught the exception with "catchException", which pushes the MiniD exception object onto the stack. We print out the error, pop the exception object, and are finished.

Is this all? Not really. See, when the catch block executes, there are no guarantees made about what data will be on the stack. Compare this to in MiniD, where all local variables within the try block are removed before the catch block is executed. Now your native function might not care what's on the stack if an exception occurs, but if it does, you'll have to handle setting the stack size yourself. This is pretty easy to do, using stackSize and setStackSize.

module example;

import tango.io.Stdout;

import minid.api;

void main()
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	// store the stack size before the try block
	auto size = stackSize(t);

	try
		runString(t, "local x = 3; x += 'x'");
	catch(MDException e)
	{
		// restore the stack before we catch the exception
		setStackSize(t, size);

		auto mdEx = catchException(t);
		Stdout.formatln("Error: {}", e);
		pop(t);
	}
}

Now the stack is guaranteed to be the same size it was before the try block was executed.

This can get kind of tedious, so minid.ex provides a function to help out. The "mdtry" function handles all the boilerplate code needed to perform "proper" exception handling. The only downside is that you can't "return" out of the try/catch/finally delegates that you pass to it, but for most cases it works fine.

module example;

import tango.io.Stdout;

import minid.api;

void main()
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	mdtry(t,
	// try
	{
		runString(t, "local x = 3; x += 'x'");
	},
	// catch
	(MDException e, word mdEx)
	{
		Stdout.formatln("Error: {}", e);
	});
}

mdtry takes two or three parameters. The first is the "try" block, the second the "catch" (which takes the D exception as well as the MiniD exception stack index, which it is not required to pop), and then an optional "finally" block (which we didn't use here).

If you just want to use a try-finally block, you don't need to do anything to inform the interpreter, so you don't need to use mdtry.

If you want to rethrow the exception, well there are two possibilites. First is if you want to inspect the MiniD exception and rethrow it. In this case, you use catchException to get it, look at it, and then use throwException (the version without parameters) to rethrow it. But the other case is if you don't even care about looking at the MiniD exception object. In that case, you don't have to use catchException and throwException at all. Just use a D throw to throw the D exception object and the stack unwinding will proceed as usual. (But in those cases, you'll probably be using scope(failure) anyway.)

Next Up

The next section shows how to create native classes and do all sorts of fun things with them.