The "Unset" Enum Item

A good bit of defensive programming is to include an "Unset" item within your enum declarations.

Unintentional Default Values - UNSAFE

Let's say you have an invoicing program that applies discounts and penalties to amounts owed.

  • If paid early, the customer gets a 2% discount
  • If paid on-time, the customer pays full price
  • If paid late, the customer pays a 10% penalty

You might represent these three possibilities with an enumerated type.  You could then write a function that took the full price the customer owed and the payment period in which they paid and produced an appropriate amount due.

Enum pp__PaymentPeriod_UNSAFE
    pp_Discount
    pp_Face
    pp_Penalty
End Enum

Private Function CalcAmtOwed_UNSAFE(BaseAmt As Currency, _
                    PaymentPeriod As pp__PaymentPeriod)
    
    Select Case PaymentPeriod
    Case pp_Discount
        CalcAmtOwed_UNSAFE = Round(BaseAmt * 0.98, 2)
    Case pp_Face
        CalcAmtOwed_UNSAFE = BaseAmt
    Case pp_Penalty
        CalcAmtOwed_UNSAFE = Round(BaseAmt * 1.1, 2)
    Case Else
        Throw "Invalid PaymentPeriod: {0}", PaymentPeriod
    End Select
    
End Function
This code is UNSAFE because if we fail to set the PmtPeriod variable, it defaults to pp_Discount (which is probably not what we want).

But what happens if we forget to initialize the PaymentPeriod value–or if the code execution resets?  In VBA, enums are little more than glorified Long integers.  That means that the default value of a typical enum is the first item in the list.  In this case, that would be pp_Discount.

Here's what could happen:

Sub UnsafeCalc()
    Dim PmtPeriod As pp__PaymentPeriod
    
    Debug.Print CalcAmtOwed_UNSAFE(100, PmtPeriod)
End Sub

When we run this UnsafeCalc procedure, which uses the above unsafe code, the value printed to the immediate window is 98.  

In other words, the CalcAmtOwed function applied a 2% discount even though we never explicitly intended to do so.  This is an example of a logic bug.  Logic bugs are the worst kind of all, because they can go undetected for so long.  

I can easily imagine an application where every few months some obscure edge case results in the PaymentPeriod variable being reinitialized and a customer receiving an unwarranted 2% discount.

An Unset Default Item: The SAFER Approach

In the code below, I've introduced an additional item in the pp__PaymentPeriod enum: pp_Unset.  The sole purpose of this item is to act as the placeholder for an uninitialized variable of this type.

Now, when we calculate the amount owed, we can throw an error if the PaymentPeriod variable has not yet been initialized.  It's still a runtime error–which we'd like to avoid–but runtime errors are better than logic errors.

Private Enum pp__PaymentPeriod
    pp_Unset
    pp_Discount
    pp_Face
    pp_Penalty
End Enum

Private Function CalcAmtOwed(BaseAmt As Currency, _
                             PaymentPeriod As pp__PaymentPeriod)
    Select Case PaymentPeriod
    Case pp_Unset
        Throw "PaymentPeriod value never set"
    Case pp_Discount
        CalcAmtOwed = Round(BaseAmt * 0.98, 2)
    Case pp_Face
        CalcAmtOwed = BaseAmt
    Case pp_Penalty
        CalcAmtOwed = Round(BaseAmt * 1.1, 2)
    Case Else
        Throw "Invalid PaymentPeriod: {0}", PaymentPeriod
    End Select
    
End Function


Sub SafeCalc()
    Dim PmtPeriod As pp__PaymentPeriod
    
    Debug.Print CalcAmtOwed(100, PmtPeriod)
End Sub
A SAFER way to work with enums.

If we run the SafeCalc routine in an immediate window, we get the following runtime error:

This is good, because the only thing worse than a bug in your software is a bug in your software that you never discover.

Alternative approach

Another option would be to override the default start value of the enum by assigning a positive number to the first item.  This achieves our objective of preventing an uninitialized pp__PaymentPeriod variable being assigned a default value of pp_Discount.

Enum pp__PaymentPeriod
    pp_Discount = 1    'Override the default start value of 0
    pp_Face            'VBA implicitly assigns pp_Face a value of 2
    pp_Penalty         'VBA implicitly assigns pp_Penalty a value of 3
End Enum

I don't like this as much because it's less explicit than having a pp_Unset item to hold the value of 0.  It's potentially misleading, too.  As a code reader, I would assume that the number 1 had some special meaning with regards to pp_Discount.  In fact, there's nothing special about the number 1 in this code other than it is not zero.  I much prefer the explicit pp_Unset enum item approach.

Side note

If you're curious about my unorthodox enum naming convention, I wrote an article about it.


Referenced articles

Throwing Errors in VBA
Introducing a frictionless alternative to Err.Raise.
Defensive Programming
Don’t build digital Maginot Lines. Program your defenses in depth.
Some Bugs are Better than Others
Not all bugs are created equal. Avoid the expensive ones by making more of the ones that are easy to find and fix.
Enum Type Naming Convention
The combination of “IntelliSense overload” and “global identifier case changes” convinced me I needed a different approach.

Image by Capri23auto from Pixabay

UPDATE [2021-07-22]: Updated the first code sample to remove the = 1 from the top enum item (h/t Philipp Stiefel).