It is sometimes said that the developer mindset is to make stuff work, while the tester mindset is to make stuff break. In modern professional software development though, it is not OK to make testing or quality control “someone else’s problem”, and it is essential to learn to build software with awareness of how to defend against the most likely scenarios that could cause an issue.

One way of describing a system’s behavior is in terms of a “happy path”, and error paths. When just starting out in software, there is a tendency to be thinking only about the happy path, and focusing on getting this to work. Defensive coding is about looking not just at what the software is supposed to do, but also at things that could prevent this happening, and dealing with them correctly. If you apply defensive coding techniques, the likelihood of errors from these predictable things should decrease significantly.

The amount of defensive coding you’ll need to do will depend on the languages and tools being used. Languages like Java for instance have checked exceptions, where a method declares in its signature the types of exceptions it can throw, and the compiler expects methods calling this method to be prepared to handle these exceptions. Languages like F# have compiler support to recognize things like missed cases in a pattern match, and a strong type system that offers build-time safety. Tools like ReSharper in the .Net ecosystem can pick up on common error scenarios, such as null checks, and suggest fixes. It’s possible to write your own Roslyn (C# compiler) extensions to do the same thing, by reading the code as it compiles, looking for possible problems, and suggesting a solution. On the other end of the spectrum, in JavaScript there are far less guarantees of what the state of something will be at runtime – with the ability to add and remove members from objects, fully dynamic typing and so on.

Defensive coding is about thinking of the things that could happen when a method runs, and putting in place a way of dealing with them where appropriate. Could this variable be null or undefined? What if this array has nothing in it? What if the database connection times out? What if that number is zero and we would normally divide by it? Note that most of these examples are error states; there’s not a lot we can do at this stage to magically fix things. What we can do though is to put in a better error message, better error handling, or go back to the user for alternative instructions to pre-empt an error. In some cases things like a retry might be appropriate, such as a transient-looking problem like a timeout or network outage.

In JavaScript, the level of checking often goes a little further. JavaScript makes this fairly easy though, with the “truthy” and “falsey” behavior; this is where coercing values to a true/false answer gives a true/false answer based on their state – for example undefined is false, where any object is true. It’s very common to see checks like: if(someObject && someObject.someMethod && typeof someObject.someMethod === ‘Function’) { someObject.someMethod() }. This is defensive code to ensure that someObject exists, that it has a member called someMethod, and the member is actually a function – if any of these preconditions aren’t met, we can choose an appropriate thing to do, without having an error thrown.

Note that defensive coding is isn’t the same as simply wrapping a try/catch around everything! The key is to respond to these cases in an appropriate way, based on the context. As always, the practice with exceptions is to only handle an exception you know how to handle. If there’s an error that you can’t recover from, the better option is to let the system crash – at least when the system crashes, you know the error isn’t simply being propagated in other ways, such as corrupting data. You might for example know that if a database query times out, it is worth simply trying again – and then if it fails three times in a row, you can report back to the user that there is a problem.

It is of course completely possible to simulate most of these error conditions, so that you know with confidence how the system will respond. It’s a good idea to write a suite of tests that inject these faults into the system, and check the response, so that there is a record of this behavior. If something does then go wrong in the system, you can start debugging by looking at the test suite and eliminating the known behaviors to narrow down the search. Most mock/stub frameworks have the ability to create objects that will respond with an error when a method is called (and it is trivial to hand-write fakes that do this). By simulating these conditions in a controlled environment, you can reduce the risks and surprises.

Share This