Defensive coding and failing fast

Hey! Today I want to quickly talk about defensive coding, and how it can make your scripts safer and stop small mistakes from spiralling out out control into Big Issues™.


So what is defensive coding?

The definition isn’t universally agreed upon, and defensive coding is often interchangeably used with the term ‘offensive coding’ and other similar terms.

For the purposes of this post, we’ll define defensive coding broadly as ‘check before you do stuff’. It’s like checking your hammer isn’t falling apart before you try to hammer a nail, so you don’t end up with a heavy hammerhead flying off and causing damage.

(in technical speak, we call those pre-checks ‘assertions’ - you’ll see the term used later on!)


With that out of the way, imagine you have a function to kill a player:

local function kill(player)
    player.Character.Humanoid.Health = 0
end

The above function isn’t very safe - if the player didn’t have a character, if they didn’t have a humanoid, if their humanoid wasn’t called “Humanoid”, it would break. Plus, you could also accidentally give it something that isn’t a player - you might accidentally try to kill a number or something!

To make it a bit safer to use, we need to check the player has a character, and that they have a humanoid:

local function kill(player)
    local character = player.Character
    assert(character ~= nil, "That player doesn't have a character")

    local humanoid = character:FindFirstChildWhichIsA "Humanoid"
    assert(humanoid ~= nil, "That player doesn't have a humanoid")

    humanoid.Health = 0
end

As you can see, I’m using a function called assert. The first argument is some condition we want to be true (in this case, that the character or humanoid exists/isn’t equal to nil). If that condition is true, then nothing happens. If the condition is false however, assert will cause an error to happen. The string we pass as the second argument is the error message to create.

“But Elttob”, I hear you say, “won’t that still break our code?”

Yup! But it’s breaking before we try and do anything. Imagine if halfway through a DataStore operation you tried to save something that can’t be saved? You’d get an error right in the middle of your saving, and could end up with some pretty big problems with data corruption. Checking that everything is alright before doing that DataStore operation would stop the code before it got to that point, saving your data and your sanity. That concept is called failing fast, and the bigger the games you build, the more you’ll grow to love it.

Now, that function isn’t perfectly safe yet, since we’re still not checking if we’re actually getting a player as the first argument. Let’s do that now with another assertion:

local function kill(player)
    assert(typeof(player) == "Instance" and player:IsA "Player", "Argument #1 must be Player")
    -- everything else from before...
end

The condition is that player must be of type Instance, and that Instance must be a Player. That’s what we put as the first argument to assert. Then, if that isn’t the case, assert will throw the error “Argument #1 must be Player”.

To be clear here, you could use any string you like for that second argument. I just prefer to write my errors that way!

Now that we’ve done that, our function is pretty safe - we can throw whatever we like at it, and it will break on purpose before we do anything. That’s how defensive coding works - to defensively code is to create code that fails fast when something isn’t right. I’ve always believed that having your game suddenly stop responding is better than having a small mistake seep into every corner of your game and snowball into a massive issue.


Of course, as with many of these coding strategies, defensive coding won’t magically save you from introducing errors into your code. All that it does is surface those errors quickly, before they can do any harm. Thanks to this, you’ll also be able to catch more errors in Studio, before the rest of the world even sees your game. It also helps when you’re debugging a bit of broken code, because you can see more clearly where things are going wrong based on your own error messages.

That means you’ll be spending less time trying to hunt down obscure hidden bugs and more time writing cool features/procrastinating on dev forums/going outside/whatever you do in your free time!


Just as a final example, here’s a snippet of code from a while back, where I’m utilising assertions pretty well:

Enjoy your freshly prepared defensive coding strategy! :sparkles:

44 Likes

I would recommend throwing errors as error(..., 2) since that will make the stack trace point to the place that called the code erroneously, rather than to the place where the assert failed.

if typeof(block) ~= "Instance" or not block:IsA("Model") then
    error("Argument #1 to 'addBlock' must be a Model", 2)
...
12 Likes

You should be mindful of overusing assert. It’s tempting to put a bunch of string concatenation in the second argument for assert. This is an issue because that obviously gets evaluated regardless of what you pass as a condition, which if you’re doing a bunch of assertions can add up quickly. This is actually such a common problem with Lua’s assert that both the officially published Progamming in Lua book and the LuaJIT wiki both warn about it. That’s not happening anywhere in this post, but for anyone else who may read this, I thought it was worth mentioning.

I would also suggest using error(..., 2), like Thomas suggested, for the same reasons. I only really find myself using assert to check the results of functions to make sure they return what I’m expecting them to, not to validate the arguments called, since the traceback isn’t very helpful for identifying what caused an error, only that it exists.

Both those things said though, I strongly agree with and recommend at the very least checking your input before using it. You’d be surprised by how many bugs are caused by something that doesn’t belong getting passed to a function (or where nothing gets passed to it). Good post.

16 Likes

I used to do this a lot actually, but at some point I switched over to using assert. Personally I don’t mind it pointing to the assert since it’s only one level deeper, but I guess it depends on what you prefer personally :slightly_smiling_face:

5 Likes

For the best of both worlds, you can write your own assertion function which uses string.format and error(..., 3):

function fastassert(condition, ...)
	if not condition then
		local var = {...}
		if next(var) then
			local success, msg = pcall(function()
				return string.format(unpack(var))
			end)
			if success then
				error("Assertion failed: " .. msg, 3)
			end
		end
		error("Assertion failed", 3)
	end
end

return fastassert
3 Likes

Here’s a cool library I wrote last year for this sort of thing

You can use it to write complex type definitions to check against. Sort of like deep typeof for tables!

8 Likes

Ah yes, I now know there’s a term for the kind of coding that prevents Big Issues™.

I will say that instead of erroring at all I like to just use if statements and handle things from there.

1 Like

Does t return some information that could be used to populate an error message like this? I think I tried this before but couldn’t figure it out.

bad argument #%d to '...' (%s)

I think getting the number of the argument is problematic with t, otherwise I’d probably use that all the time over writing out the checks.

First off, I just want to say thank you for sharing what you’re doing :slight_smile:

To add to it, I think its worth mentioning a very important note about asserts, which is you are never ever meant to put code inside an assert block that you need to execute.

eg:

assert(   TryToSpawnPlayer(),  "Player failed to spawn" )

Because in most settings that are not roblox :wink: code inside the assert() is skipped entirely when you do a release build.

Anyhow, the other thing I wanted to suggest was that a more practical defensive pattern for roblox is not to use asserts too much at all, and rather rely on a “If error warn and return” type defensive strategy, because of how many errors in roblox that are caused by the streaming+networked nature of the platform.

7 Likes

Yes! Every t function returns either true or false, string where the string is an error message.

EDIT: After rereading your reply, you might want to override t.tuple’s logic to get what you want.

3 Likes