BrainsToBytes

Defensive Programming Fundamentals

When your program starts running in production, the conditions and cases you thought impossible will happen, period. In reality, it's impossible to ensure that your code will have optimal running conditions all the time. As a developer, you'd like to ensure your projects will be able to cope with the imperfections of the real world. That is why techniques like defensive programming will always have a place in your toolbox.

This approach is the programming equivalent of defensive driving. In defensive driving, you take the responsibility of anticipating risky conditions and mistakes made by other drivers. In other words, you take responsibility for protecting yourself from other driver's mistakes and bad driving conditions.

Defensive programming, by comparison, requires you to recognize that bad things will happen to your program and design accordingly. You must anticipate and prepare for cases such as data corruption, connection timeouts and passing wrong data into a function. By deliberately handling cases that "will never happen"  your code will be prepared to fare better under those circumstances.

Garbage in, nothing out

One of the simplest defensive programming techniques is checking preconditions and postconditions in your functions. A precondition is something that needs to be true before your function code executes. A postcondition, on the other hand, is something that must always be true after your function finished execution. To illustrate where those checks happen, take a look at the following snippet.

def function_name(arguments)
  #check preconditions

  #function body 

  #check postconditions
  
  #return
end

Generally, these checks are for assumptions like:

  • The input parameters are between an expected range
  • A pointer is not null
  • The function's logic didn't alter variables that shouldn't change
  • The function properly released an external resource handler (like a file handler)
  • The result of a calculation satisfies some numerical check, like a CRC
  • Containers like lists or stacks have the required number of elements

Using both types of check in the same function is not mandatory or even necessary. For example, the following functions make use of only one check each.

# Precondition check: ensure the input falls between a given range, in this case, higher than 0
def square_root(radicand)
  if(radicand < 0)
    #Handle the error
  end
  #function body
end
# Postcondition check: verify a variable didn't change
def no_side_effects(should_not_change)
  should_not_change_initial_value = should_not_change

  #function body  

  if(should_not_change != should_not_change_initial_value)
    #Handle the error
  end
  
  #return
end

These simple checks give you lots of power by explicitly stating what to do when something bad happens. Using them is a sure way to toughen up your functions and make your code more secure.

For exceptional cases, use exceptions

Exception handling is an incredibly powerful feature most modern programming languages include. Because it's a very big topic, and implementations differ, I won't be able to cover it in a single article. Still, if you want to learn more about the topic, take a look at these awesome resources:

There are a couple of ideas for making the most of your exceptions regardless of which language you use:

  • Remember an exception's most important feature: once you fire it, you can't ignore it. It forces you to deliberately handle the error.
  • When throwing the exception, include all the necessary information to understand what went wrong. Avoid writing code like the following snippet:
// Catch blocks (or their equivalent) must at least log the exception message. 
public void badUseOfExceptions() {

      try {
          // Perform some operations that might throw exceptions.
      }
      catch (Exception e) {
          // Do not leave this empty, at least log a message to understand why the program threw the exception
      }

   }
  • Even though exceptions are an extremely powerful tool, you need to use them with caution. Overusing exceptions can make your program harder to follow and mangle the execution flow.
  • Exceptions weaken encapsulation by requiring the code calling a function to know which exceptions it might throw. For instance, if you can handle the error locally, do so.
  • Exceptions (as their name imply) are meant to be used only in exceptional circumstances. When in doubt, ask yourself the question: if I remove all the exception handlers, will my program run? If your answer is no, you might be using them in unexceptional cases.

Handle the error, don't ignore it

So, your code makes good use of exceptions and all your functions are rock-solid. Depending on the type of error or data you are manipulating, you can decide to do things like:

  • Return a neutral or harmless value, like a 0 from a numerical calculation.
  • In stream processing, return the last piece of valid data.
  • Return the closest valid value. Like returning 0 from a thermometer app (in Kelvin) that finds a negative value for temperature.
  • Log a warning message.
  • Return an error code for the caller to handle it or throw an exception.
  • Shut the system down.

The approach you choose for handling the error depends on your robustness and correctness needs.

Is your code robust, or is it correct?

Robustness and correctness are qualities that will significantly influence the way you handle errors in your code.

A software system is said to be correct if it never returns an inaccurate result. A robust system, on the other hand, continues to operate even if it leads to inaccurate results.

The strategy you choose for error handling will tend to favor one property in detriment of the other. For example, returning an empty string in case of error might be incorrect, but the program will keep running. You improved the code's robustness in detriment of its correctness. In contrast, shutting down the program will prevent returning bad data, making the program more correct and less robust.

The need for robustness and correctness varies by application. It's fine when a video game keeps running in spite of coloring a pixel the wrong shade of green. Conversely, you don't want radiation machines operating even if the error is small. In general, safety-critical software favors correctness, whereas consumer applications favor robustness.

A bit of attention to detail makes all the difference

Defensive programming is a big topic, but you don't need to tackle the whole corpus of knowledge in one sitting. The learning process is incremental; every new technique you learn is a valuable addition to your toolbox.

Whenever possible, pay attention to how your program handles errors, be intentional and leave nothing to chance. The investment is small compared to the returns you will get once your program starts to run in production.

What to do next:

  • Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
  • Carl Vitullo wrote an excellent article about the problems of overdoing defensive programming, you can find it here.
  • The good folks at Jobtensor wrote a neat tool for learning the basics of Python and some popular libraries like Pandas and Numpy. You can find it here.
  • You can find more information on error handling in chapter 7 of Clean Code, and about defensive programming in chapter 8 of Code Complete. This and other very helpful books are in the recommended reading list.
  • Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!
Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!