Guard Clauses
Guard clauses are one of my favorite low-friction defensive programming tools.
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.
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:
- Exiting silently
- Showing a message then exiting
- Raising an error that can bubble up the call stack
Guard clauses are a form of defensive programming and are an important part of building "defenses in depth."
Advantages of Guard Clauses
- 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").
- 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.
- 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. - 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:
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.
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.
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.