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!