Defensive Programming 101: Guard Clauses

Defensive Programming 101: Guard Clauses

·

4 min read

Introduction

A guard clause is a conditional check that runs before your method executes its main processing code. They allow us to follow the guidance of failing fast as we can pre-emptively check conditions that must hold for our methods to complete sucessfully. In doing so, we can capture more context, avoid wasteful executions that are guaranteed to fail and prevent code from running that may need to be undone if it fails partway through.

Exception Based

Throwing an exception when your conditional check fails is a strong way to report an issue. Be sure to use an appropriate exception class and provide a suitable amount of detail. You could skip a check, if the methods you are calling from your code handle the same condition and it does not make sense to fail fast for the reasons outlined above.

Pros

  • They allow you to detail why a method failed, both in the exception you choose to throw and the information you instantiate it with
  • There are a wide range of exceptions that suit most needs out of the box
  • They don't require you to change the return type of your method

Cons

  • They cannot be used to report success and as such they cannot detail how one of multiple success states was reached
  • Requires additional handling to prevent them disrupting the program

Example

public void DoWork(string argOne, object argTwo)
{
    if (string.IsNullOrWhitespace(argOne))
        throw new ArgumentException($"{nameof(argOne)} must be populated.");

    if (argTwo == null)
        throw new ArgumentNullException(nameof(argTwo));

    // Perform process
}

Boolean Based

If all you need is an indication of success or failure, boolean based guard clauses will work. You should be sure this is exactly what you need before implenting it as you lose out on detail you can use to inform users, log for later analysis, debug with and influence retry attempts.

Pros

  • They are quick and simple to implement

Cons

  • They offer no detail regarding why a method failed or succeeded in its execution
  • Readability can suffer when checking multiple conditions
  • They require you to change the return type of your method

Example

public bool DoWork(string argOne, object argTwo)
{
    if (string.IsNullOrWhitespace(argOne) || argTwo == null)
        return false;

    // Perform process

    return true;
}

Result Based

This option sits between the hard failure reporting of exceptions and the soft failure reporting of boolean returns. It allows you to return a response indicating success or failure as well as detailing why these states were reached.

Pros

  • They allow you to detail why failure states were reached
  • They also allow you to detail why success states were reached, which is useful if there is more than one way your method can successfully process

Cons

  • They require an extra class/record
  • They may take a bit of work to get to a resuable state for the needs of your project without coding in details specific to some methods and not others
  • They require you to change the return type of your method

Example

The Result record:

public record Result(bool outcome, string detail);

A method using it:

public Result DoWork(string argOne, object argTwo)
{
    if (string.IsNullOrWhitespace(argOne))
        return new Result(false, $"{nameof(argOne)} must be populated.");

    if (argTwo == null)
        return new Result(false, $"{nameof(argTwo)} cannot be null.");

    // Perform process

    return new Result(true, "Work completed.");
}

Tips

  • If you find guard clauses are adding a lot of bloat to your methods, consider that they may have too many responsibilities
  • You may find that not every method requires guard clauses because they are not doing particularly critical work and/or it is ok to let the methods you call from your code handle it
  • For a given path through your code, it may make sense to keep guard clauses to one layer rather than many. This reduces the amount of redundant and repetitive code you will have to write

Conclusion

Out of the three options presented above, I would suggest using Exceptions or Result Objects over Booleans for a serious project. In most cases, having detail as to why something failed will be valuable in working with any areas of the code that are frequently failing.