Defensive & offensive programming

Defensive programming is a term that many programmers have heard of. It’s related to error handling and having correct programs. For some programs, defensive programming is essential. For others, it may be useful to use here and there. Along with that, there’s also offensive programming.

In this article, we’ll start by examining "normal programming". We’re examining it first because some people mistake it for defensive programming. However, this is something that you should do regardless of whether you do defensive programming or not.

Then, we’ll examine defensive programming, followed by offensive programming.

Normal programming

Normal programming means to have all of the checks that are necessary in your code. It also means to handle certain errors.

Necessary checks in code

Some code needs a lot of conditionals. It can feel like you’re being "overly defensive" with the number of conditionals you have.

One example of this is checking for null (the billion-dollar mistake). Nulls and null checks are very tricky. Many codebases need if statements for them all over the place.

Another example is validating user input. You need to have many checks to ensure that user input is valid. Your program needs to handle it very harshly. Otherwise, you’ll have security vulnerabilities.

But that’s not defensive programming.

Rather, something like forgetting a single null check is a bug. They’re not unnecessary checks that you do "just in case". They’re necessary checks. The value will be null sometimes and that’s normal. If you forget a single one, you have a bug. No questions asked.

Necessary error handling

Error handling is very important in programs. You always need to consider how your program should respond to errors.

This also depends on the kind of error.

Generally, most programs handle "expected errors" which are out of their control. For example:

  • failing to send a network request because the network connection dropped
  • failing to find a file because a user deleted it

It would be very bad for the user experience for a program to crash on these. Also, it’s relatively easy to handle them.

As a result, most programs handle these, even if they’re not doing defensive programming. So, again, this is considered "normal programming", not defensive programming.

A different kind of error is a bug. In most programs, these errors are considered "unrecoverable". The rule-of-thumb for most programs is to crash on these errors and to not handle them.

Defensive programming

In my interpretation, defensive programming is about fault tolerance. It means going above and beyond to ensure that your program continues working. It’s used for certain programs where you need maximum:

  • availability
  • safety
  • security

One example of this, as Adrian Georgescu writes on his post on NASA coding standards, is code used in space exploration missions.

That code is developed once and sent to space. If it goes wrong, that’s billions of dollars worth of work lost.

For that kind of code, you need to take extreme measures. The code must work correctly, without crashing, no matter what.

This is very different to your average program. With your average program, if you have a bug, no problem. You just crash the program. Then, some other process can start it again. In the meantime, you have 5 other instances of your program running on different servers. Everything works even if an instance or two crash. In a really bad case, you can even go to the physical server and restart it. In the worst case, you can always update the code.

But, with certain critical software, you can’t do that. The software has to always work properly.

The problem is that we aren’t perfect. We create bugs. Not to mention that other errors may occur that are outside of the program’s control (such as operating system errors). This means that the program may fail.

But, that’s not an option with some software.

As a result, you need to do everything in your power to prevent failure.

Extra checks and recovery mechanisms

The problem is that the software will fail in unexpected ways. You’ll have unexpected errors and bugs. After all, if you expected them, you wouldn’t need defensive programming. You would fix the bugs and you’d handle the errors which are outside of your program’s control. You’d be doing "normal programming" and everything would work.

But, unexpected errors means that you won’t expect them. You won’t know what kind of errors will come up and where.

So, you’ll have error handling and recovery mechanisms just in case there are errors. You’ll be doing risk analysis with your code. You’ll be thinking things like:

  • what code could fail?
  • how likely is it that it will fail?
  • how confident am I that certain code is safe and won’t fail?

You’ll have recovery mechanisms for code that is likely to fail. You might also have "global" or "catch-all" recovery mechanisms in case anything else fails.

How to do defensive programming

Part of defensive programming is checking that things are correct. Another part is recovering if they’re not.

Example checks

Here are some (simple) examples of things you might check with defensive programming. You wouldn’t normally do these with "normal programming".

For a real program, you’ll have to consider what you need to check and where. Check everything that you think is necessary.

Example with checking function arguments

You can check whether a function was called with valid arguments. The arguments should have the correct type and range.

Here’s a code example:

function foo(nonEmptyString, naturalInteger) {
  if (
    typeof nonEmptyString !== 'string' || // if it's not a string
    nonEmptyString === '' || // if it's the empty string
    !Number.isInteger(naturalInteger) || // if it's not an integer
    naturalInteger < 1 // if it's not a natural integer (1 or more)
  ) {
    // code to handle invalid arguments here
  }
  // code for normal function execution
}

These checks are "defensive". That’s because, if a function is called with invalid arguments, that’s normally considered a bug. "Normal programming" doesn’t generally handle bugs.

Example with checking data

Another example is when working with data.

Normally, you would only check some data when you first receive it. For example, if a user submits some data, you would check it to make sure it’s valid.

Then, you would work with that data. You might format it or transform it in some way. You would have tests to make sure that these processes work correctly.

In theory, you shouldn’t need to also check the final result. The initial data is valid. The code you process it with works correctly. Therefore, the end result should be correct.

But, if you’re doing defensive programming, you might have checks on the final result too.

Recovering

Recovery is one of the differences between normal and defensive programming. "Normal programming" doesn’t generally try to recover from bugs. It usually crashes the program or silently ignores them. However, defensive programming tries to recover.

With defensive programming, you need to fix things so that the program can continue executing correctly.

For example, if some data that you obtained was invalid, you might:

  • discard it and try to obtain new data instead
  • try to restart the data-gathering program / mechanism and then try again
  • try to manually fix the state of the data-gathering program without restarting
  • use a backup program while the primary one is restarting
  • contact a server and obtain some data from that instead
  • or anything else

For more information on recovery, please see how to respond to errors.

Other requirements of defensive programming

When doing defensive programming, you also need:

  • very strict development standards
  • tons of tests
  • good general software quality
  • source code that’s easy to understand
  • software that behaves in a predictable manner

Those points are important for all software. However, they’re critical for defensive programming. After all, if your source code isn’t well tested or easy to understand, it could have bugs. This defeats the point of defensive programming.

Downsides of defensive programming

Defensive programming has significant downsides. Some downsides are that:

  • it requires a lot more code. At the very least, you’ll have many more conditions and checks than a similar program without defensive programming.
  • performance can be worse. That’s because the extra checks take time to execute.
  • it takes much longer to implement. You’ll need to spend a lot of time to:
    • analyse all of the ways that the code can fail
    • consider how you can recover safely
    • implement error handling

These downsides can be significant. For many programs, thorough defensive programming isn’t realistic.

Offensive programming

The goal of offensive programming is to catch bugs and crash early. As explained in how to respond to errors, crashing early is helpful.

It means that you are notified of bugs immediately. Also, the stack trace that you get is closer to the source of the error. This helps with debugging.

How to do offensive programming

To do offensive programming, you:

  • do normal programming
  • avoid defensive programming (don’t recover from bugs)
  • write code in a way where bugs are obvious and easy to find
  • immediately crash the program on bugs

Just like with normal programming, you still need conditionals for things that aren’t bugs. For example, you need conditionals for null checks.

Similarly, you should probably handle errors which aren’t bugs. Most of the time, it would be unreasonable to crash on them. In other words, you should probably follow the "normal programming" way of dealing with these.

Also, you should write code in a way where bugs are easy to find. Here are some techniques for that:

Avoid fallback code and default values

Things like default state, default arguments and fallback code can hide bugs.

For example, you might call a function with incorrect arguments. You might have accidentally used null instead of a string for an argument. That’s a bug. However, due to default arguments, the function will execute anyway. The bug won’t be caught and the program may be doing the wrong thing.

A similar thing applies to fallback code. One example is inheritance and subclassing. You may have forgotten to implement a method in a subclass. Then, you call the method and it executes the parent’s method. That’s unintended behaviour, which is a bug.

To prevent this, avoid using things like default state, default values and fallback implementations.

Avoid checks on code that will crash on errors

Sometimes, buggy code will crash on its own. You don’t have to do anything extra. Leave the code as it is and let it crash.

For example, consider the code below. arg should never be null. If it’s null, that’s a bug.

If you have a defensive check around it, the code won’t crash:

function foo(arg) {
  if (arg !== null) { // code doesn't crash if arg is null
    return arg.bar();
  }
}

But if you don’t have a defensive check, the code will crash.

function foo(arg) {
  return arg.bar(); // code crashes if arg is null
}

You want the code to crash. So, in this case, just leave it as it is without a defensive check.

Have conditionals or assertions to check for errors

Contrary to the point above, some bugs won’t cause the program to crash.

For example, you might have some incorrect state in your program. Your program may not crash from that.

As another example, some code may execute that shouldn’t execute under normal circumstances.

In these cases, you can use manual checks. Then, if you find something wrong, you can manually crash the program.

For example:

function foo(arg) {
  switch(arg) {
    case 'foo':
      // do something
      break;
    case 'bar':
      // do something
      break;
    default:
      // this code should never execute, so crash the program
      throw new Error('Default case should never execute.');
  }
}

Here’s another example with checking state:

function getCurrentPlayerHealth() {
  const health = player.health;
  if (health < 0 || health > 100) {
    // this condition should never evaluate to true, so crash the program
    throw new Error(`Player health should be between 0 and 100.`);
  }
  // continue normal function execution
}

More traditionally, these kinds of "bug checks" use assertions instead of conditionals.

Assertions are bug-finding tools. If they fail, they signify a bug. Conditionals are control-flow tools. If a conditional "fails", it doesn’t signify a bug.

So, instead of using conditionals, you can use assert statements. For details on how to do this, please see the documentation for your programming language.

In some programming languages, assertions crash the program. However, in others, they don’t crash it. They may only print an error message to the console or something. Both are usable. However, offensive programming recommends hard crashing when possible.

Also, some programming languages allow you to turn off assertions in production for better performance.

Downsides of offensive programming

Similar to defensive programming, offensive programming has downsides.

One downside is having to crash the program. As explained in how to respond to errors, crashing on bugs is usually good. However, it might be something that you’re not prepared to do in your application.

Another downside is performance. Having assert statements throughout your code can significantly reduce performance.

Don’t worry if you’re not willing to accept these downsides. Many programming languages don’t crash when assertions fail. Also, they have the option of removing assertions from production code.

When to use offensive programming

Offensive programming helps you catch bugs. That’s a significant win.

For this reason, it’s good to use it during development. Generally, you’ll put assert statements here and there to ensure that certain things are correct.

As for production, it depends. Consider the pros and cons of offensive programming and make your decision.

It’s alright to only use offensive programming in development. After all, catching more bugs during development is better than nothing.

Be pragmatic

When choosing your approach to handling errors, you need to be pragmatic.

"Normal programming" is the minimum that you need to do for most programs.

For some programs, you might use defensive programming. In particular, for programs that need high:

  • availability
  • security
  • reliability

But also understand the downsides. Primarily, the downsides are worse performance and longer development time.

Offensive programming helps you catch bugs. This is useful during development (and even production).

You can mix and match the approaches based on what you need. You can even use different methodologies in different areas of the code. It’s up to you to decide.

Final notes

So that’s it for this article. I hope that you found it useful.

As always, if any points were missed, or if you disagree with anything, or have any comments or feedback then please leave a comment below.

For the next steps, I recommend looking at the other articles in the error handling series.

Alright, thanks and see you next time.

Credits

Image credits:

  • Turtle in sea – Photo by Tanguy Sauvin from Pexels
  • Turtle in shell – Photo by Hogr Othman on Unsplash
  • Tiger – Photo by Samuele Giglio on Unsplash
  • Squirrel – Photo by Pixabay from Pexels
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments