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.
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.
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.