Exception Inheritance in GML

Exception Inheritance in GML

GameMaker has try/catch exception handling, which is useful any time you deal with code that have complex failure modes that you can’t realistically be checked for. But it's also, like anything in programming, a potential pitfall. I want to dedicate this article to talking about what to look out for, and how to make the most out of exception handling in GML, and some techniques other languages use to make this an expressive tool rather than a clumsy one.

Before I get into that, I have to first note that the well is deep when it comes to programming philosophy and ideological debate about whether you should use try/catch versus other ways of writing error handling. Many people will say that try/catch exception handling is the literal demon scourge, and that if you use it you are bad and should feel bad. Some of the key arguments include: if you know there’s a way for your code to raise an exception, you should simply just check for it since it is no longer an exception; or that try/catch introduces another path for the program to flow, making it hard to reason about; or that try/catch is slow (that's a weird myth, in GML, it's actually fast and potentially faster than alternatives!)

However, I believe that at least for now, because of the way GameMaker's built-in functions work, and because of a lack of language features like destructuring, it is a pragmatic choice to use try/catch exception handling while writing GML, despite the drawbacks that people describe, and there are a few things you can do to avoid the pitfalls and make it useful.

Crashing is good, actually: Overbroad catching

The most important thing to watch out for when using try/catch is the fact that it will catch any and all exceptions, including a lot of errors that you ideally shouldn't be catching. This is called over-broad error catching, and what makes try/catch an inaccurate tool if not used carefully.

For example, let's take a case where try/catch is often needed: json_parse(). GameMaker's json_parse() function doesn't have a sentinel return value for when the input is invalid JSON. Instead, it'll throw an exception. The only way to prevent this from crashing your game is to do json_parse() inside of a try/catch.

So, let's say you have the following code:

try {
  var _data = json_parse(_string);
}
catch (_err) {
  // do something
}

Looks straightforward enough, right? Surely not much can go wrong?

Here's a problem: this catches everything. Even for trivial code like this, there are at least two errors possible that you probably don't want to catch: if _string doesn't exist as a variable (maybe you made a typo), and the more obscure problem of if _data is a function parameter, which means you cannot declare it again as a local variable with var _data. Imagine if you were doing more stuff between the try and the catch, there's way more that could go wrong that you would be accidentally catching.

Both of these errors, if you make them, will be caught by try/catch and unless you are careful about how you display the error message, you may end up hiding these bugs from yourself: you've made the game not crash when the JSON is invalid, but you've also made the game not crash when you have an actual bug in your code; a situation where you actually want the crash to happen to inform you of a code problem to fix.

So try/catch is a clumsy tool by itself. It's over-broad. And the thing that you should keep at the front of your mind when writing a try/catch is: how do I make this as specific to the exact problem I want to catch as possible?

Catch and rethrow

The throw keyword lets us throw exceptions when we want the code to signal an abnormal situation that it can’t handle. These are what are triggering the catch to happen. json_parse() is doing that throw internally, but you can also throw in our own code too, and importantly, you can also rethrow inside a catch. This allows us to catch an exception, examine it to decide whether it’s something the code can handle, or rethrow it again if it isn’t to allow some outer scope to handle it or crash the game.

For example:

try {
  do_something();
}
catch (_err) {
  if (is_struct(_err) && _err[$ "message"] == "JSON parse error") {
    // handle it
    show_debug_message($"whoops {_err.message}");
  }
  else {
    // rethrow it
    throw err;
  }
}

Here, we examine the format and contents of _err to see if it is the specific thing we want to catch, and if it is, we "handle" it (for the purpose of this example, just showing a debug message), and everything else we don't want to catch we re-throw so that something further up the call stack might handle it, or let it crash the program if nothing wants to catch it.

GameMaker's built-in json_parse() function will throw an error struct that looks like this:

 { stacktrace : [ "gml_Script_anon@1204@gml_Object_obj_demo_Other_4 (line 39)","gml_Object_obj_demo_Other_4 (line 46) - a();
" ], script : "gml_Script_anon@1204@gml_Object_obj_demo_Other_4", line : 39, message : "JSON parse error", longMessage : "ERROR in
action number 1
of Other Event: Room Start
for object obj_demo:


JSON parse error
 at gml_Script_anon@1204@gml_Object_obj_demo_Other_4 (line 39) - 		json_parse(_string);
" }

So if we want to catch just this exception and no others, we can have our catch code inspect the exception for the correct message property, and rethrow everything else.

This is fine and great for dealing with GameMaker's built-in errors. But as always, there are ways for us to use a similar but better thing for our own functions and libraries.

Throwing structs with constructors

In the above section, that struct that was being thrown was a struct generated by GameMaker's built-in functions. We don't have a lot of options when it comes to those, we just have to handle whatever GameMaker generates. But for our own code and libraries, we get to choose what we throw. We can throw any type of variable: a string, a number, a struct, and others.

throw "hello";

throw -1;

throw {error: "oops"};

The third example above will cause the error in the nearest catch to be the struct {error: "oops"}. And of course, if we can throw structs, we can also use constructors to create these structs.

For example, let’s say we defined our own constructor for exceptions:

function Execption(_message) constructor {
  self.human_friendly_message = _message;
}

Then we can catch it and inspect its values:

try {
  throw new Exception();
}
catch (_err) {
  if (is_struct(_err) && is_string(_err[$ "human_friendly_message"])) {
    // the exception is one of our custom human-friendly ones
    show_alert_to_user(_err.human_friendly_message)
  }
  else {
    // rethrow
    throw _err;
  }
}

Of course, it's not very useful to just try { throw }, but imagine that it was some more complex code in there or function calls and there's a throw or two somewhere deep inside.

We've defined a constructor that can be used to construct exception structs in exactly the format that we want. We can use it to store whatever information that is necessary for us to handle the exception later (like a nice human-friendly messages to show to the player), and it could also include other information that can help us handle the exception.

Exception inheritance

In the above examples, we check that the exception is a struct with specific properties, using a combination of is_struct() and other checks. But we're not limited to doing this. Because we're throwing using a constructor, we can use GML's constructor inheritance checking function is_instanceof()

try {
  ...
}
catch (_err) {
  if (is_instanceof(_err, Exception)) {
    // the exception is one of our custom human-friendly ones
    show_alert_to_user(_err.human_friendly_message)
  }
  else {
    throw _err;
  }
}

Using is_instanceof lets us check that the struct came from a specific constructor, or any of its children. That latter point is important: it means we can check for child constructors whose parent is Exception.

For example, let's take the hypothetical situation described in the previous Sentinels post, which returned a special sentinel values when an equipment slot doesn't exist. Instead of returning a sentinel, we could throw an exception instead (which may be a cleaner way to handle that particular situation than using a sentinel return value). We could define a child exception to the custom Exception constructor, and bake-in the error message to avoid having to type it out every time.

function NoSlotException(): Exception("That slot doesn't exist") {};

Now, every time we need to alert the user of the code of a no-equipment slot exception, we can throw new NoSlotException() (without needing to provide the actual text), and the error message it produces will be standardized. The end-user of the code can then check for this specific exception using an is_instanceof()

try {
  ...
}
catch (_err) {
  if (is_instanceof(_err, NoSlotException)) {
    // that inventory slot doesn't exist, so skip it
    draw_empt_slot();
  }
  else {
    throw _err;
  }
}

Exception Inheritance Trees

Being able to not only test for specific exceptions, but also children of specific exceptions is extremely useful when writing and using libraries, since very often you (the library author) don't know ahead of time exactly how the code will be used, and what exceptions might be okay and what exceptions are not. By throwing exceptions that the end-user of the code can check for, it can be left to the end-user of the code to handle exactly the ones they want to handle and let crash the ones that should crash.

For example, a library written to handle save file loading could detect and throw multiple types of exception, such as:

  • the file not existing
  • the format being unreadable
  • the expected values being missing
  • the save version being incorrect

These might all need to be handled in a different way by the end-user of the library, but at the time of writing of the library, you don’t know for sure. So you could make a tree of exceptions using constructor inheritance, throw these at the relevant parts of the library code, and let the end-user check for them:

// our base exception for the savel/load library
function SaveSystemException() constructor {}

// base exception for file-related issues
function SaveSystemFileException(): SaveSystemException() constructor {}
function SaveSystemFileNotFoundException(): SaveSystemFileException() constructor {}
function SaveSystemFileUnreadableException(): SaveSystemFileException() constructor {}

// base exception for format-related issues
function SaveSystemFormatException(): SaveSystemException() constructor {}
function SaveSystemFormatExpectedValueMissingException(): SaveSystemFormatException() constructor {}
function SaveSystemFormatSaveVersionException(): SaveSystemFormatException() constructor {}

The end-user could opt to check only for the base exception, or they may want to implement different behaviours based on the exact exception being thrown. Whatever works for them.

We could even go further, and inside the save file loading library, we could help the user out by translating a GameMaker exception into our own. So that original json_parse() exception handling from earlier on, we could re-write inside our library to rethrow a custom exception instead of the default GameMaker one:

try {
  _data = json_parse(_string);
}
catch (_err) {
  if (is_struct(_err) && _err[$ "message"] == "JSON parse error") {
    // convert into a custom one and rethrow
    throw new SaveSystemFormatException();
  }
  // rethrow everything else
  throw err;
}

More Resources

Here's a Gist from Nomm, who has some custom exception constructors that you can use in a similar way to described in this post, with extra features.

Exception class for GameMaker 2023, allows custom message or wrapping runtime errors. Allows for error script to be loaded into memory
Exception class for GameMaker 2023, allows custom message or wrapping runtime errors. Allows for error script to be loaded into memory - Exception.gml

I'm not just making up these techniques. They're the same ones used by JavaScript, Python, and other languages. Python in particular has an extensive exception tree, which it not only uses in its standard libraries, but also there is a well-established convention that Python library writers follow, to make use of the standard exceptions in their own code. For example, you can reasonably assume that a python library someone wrote that handles files would throw a FileNotFound error for situations of that description.

Exception Handling in Python with Examples - Dot Net Tutorials
Example exception tree that's provided by Python's standard (built-in) library

Now you know: you don't have to just throw a string, you can throw whole-ass structs with constructors; and by checking for the specific exceptions, GameMaker's try/catch exception handling can be used to give your library or game code more expressive ways to handle exceptions.