Guard Clauses

A guard clause is a conditional statement at the top of a routine that validates the routine's arguments, user inputs, or other types of program state.

Private Sub PreviewBtn_Click()
    If ControlIsBlank(Me.cbCustomerID) Then Exit Sub  '<-- Guard clause
    
    PreviewReport "AccountSummary", "CustomerID=" & Me.cbCustomerID
End Sub   
This code uses my custom ControlIsBlank() and PreviewReport() functions to open a customer's Account Summary report. The guard clause prevents the report from opening if the user did not select a customer from the cbCustomerID combo box.

If the guard clause detects a problem, it will prevent the rest of the routine from running.  Depending on the situation, it will do this by:

Guard clauses are a form of defensive programming and are an important part of building "defenses in depth."

Advantages of Guard Clauses

  1. Validates preconditions: Similar to the Design by Contract™ concept from Eiffel Software, guard clauses ensure that the calling code/end user are meeting their obligations by providing valid inputs and program state (refer to Section 3 - The Notion of Contract in Eiffel's "Introduction to Design by Contract").
  2. Provides built-in documentation: Like Doc Tests, guard clauses provide verifiable information about your routines.  In this case, what inputs and program state your procedure will–and will not–tolerate.  Whereas doc tests verify function outputs, guard clauses validate procedure inputs.
  3. Reduced levels of indenting: Rather than wrapping the entire routine inside a big If...End If block, a guard clause exits the routine before the main code is even reached.  This lets you forego the additional level of indenting, which makes the code more readable and easier to follow.
  4. Logical separation from the routine's main purpose: By performing all of your validity checks at the top of the routine, you can avoid mixing those validity checks into your main code.  This helps boost the signal-to-noise ratio.

Guard Clause Helper Functions

I wrote several functions specifically to use with guard clauses.

The first two functions are meant to be called as part of the conditional part of the guard clause (i.e., between the If and Then keywords):

  • ControlIsBlank(): often used on unbound forms to verify that a user has completed all required fields before performing some action
  • SaveFails(): attempts to save changes to a bound form (via Dirty = False) and returns True if the save attempt fails; this unusual function name makes more sense when read as a guard clause: If SaveFails(Me) Then Exit Sub

The next two routines are called as alternatives to Exit Sub or Exit Function if the conditional detects a problem:

  • Throw(): a frictionless way to generate deterministic error numbers from custom error messages (useful when integrating with bug databases)
  • Alert(): same as the Throw() function above (raises an error that will bubble up the call stack), but will not log the error to the bug database (used for things like warning a user that they provided an invalid value where we don't want to clog up the bug database with irrelevant noise)

Guard Clause Examples

I use guard clauses frequently in my code.  A search for "guard clause" on this site currently returns 15 results.  Here is a sampling of some of those articles and their accompanying code samples:

The ArrowKeyNav Routine

Sub ArrowKeyNav(ByRef KeyCode As Integer, Shift As Integer)
    If Shift <> 0 Then Exit Sub

    'Remaining code available at: https://nolongerset.com/arrowkeynav-routine/
This checks to see if the user is holding down the [Ctrl], [Alt], or [Shift] keys at the time they press the up or down arrow key.  If they are, we exit immediately without overriding any behavior.

The MakeSurePathExists Function

Sub MyRoutine(MyFolder As String)
    If Not MakeSurePathExists(MyFolder) Then Exit Sub
    
    'Do stuff with MyFolder
End Sub
Pass the folder you want to verify exists [to the MakeSurePathExists function].  To be on the safe side, you should check the return value to make sure it worked.  In practice, I often use this as part of a guard clause.

The DefocusIfActive Routine

Sub DefocusIfActive(Ctl As Control, Optional FallbackCtl As Control = Nothing)
    'We only want to change focus if the passed control is active
    If Not Ctl Is ActiveControl Then Exit Sub
    
    'Remaining code available at: https://nolongerset.com/defocusifactive/
The procedure starts by checking to see if the passed control is currently active.  We don't want to switch the user's active control for no good reason.  If the passed control is not active, we exit immediately.

The GetAttr Function

Function GetAttr(AttrName As String, AttrVal As String) As String
    If Len(AttrName) = 0 Then Throw "Missing attribute name"
    
    If Len(AttrVal) > 0 Then GetAttr = AttrName & "=" & Qt(AttrVal)
End Function
You'll need my Throw() routine if you want to leave the guard clause in place as the first line of the function.  You can always remove that line if you want, but I try to err on the side of programming defensively.

Cover image created with Microsoft Designer.  Image by PublicDomainPictures from Pixabay.