Bug Alert: Accidental Double-Clicking of Long-Running Processes

In the spring of 2021, an ear-splitting screeching filled the otherwise quiet night air in and around the District of Columbia.

Perhaps surprisingly, the irritating noise had nothing to do with the acrimony of American politics.  Rather, it was the sound of billions of periodical cicadas–a group known as Brood X–emerging from 17 years of underground dormancy.  

These bugs are ever-present just below the surface of the earth.  We humans walk above them, blissfully unaware of their existence.  

Until one day–like some hallucinating entomologist's fever dream–they emerge in unison to spread chaos: the aforementioned 96-decibel mating calls; swarms so thick they cause car crashes; and, of course, the cacophony of calls to abandon beef in favor of bugs as a climate-friendly protein alternative (knock yourselves out, folks, that's just more rib eye for me).

The life cycle of these cicadas is a curiosity of biology.  

It's also an apt metaphor for today's article.

Logic Errors: The Most Insidious of All Bugs

A logic error is a piece of code that executes properly but does not behave the way you intended.

Logic errors are the worst kind of errors because they can go unnoticed for so long.

And among logic errors, the most infuriating to identify, debug, and fix are concurrency errors.

You may be thinking we don't have to worry about concurrency errors as Access developers.  After all, Access is single-threaded.  But, Access is also event-driven.  And, if we're not careful, the event-driven nature of Access still allows us to unwittingly introduce race conditions into our code.

The Accidental Double-Click of a Long-Running Process

Every few years, this bug pattern comes around to bite me.

Here's what the pattern looks like:

  • User clicks a button that starts a long-running process
  • Before the process can finish, the user clicks the button again, causing trouble

The nature of the second click takes a few different forms:

  • A user who routinely double-clicks everything
  • A finger twitch that leads to an accidental double-click
  • An impatient user who clicks the button a second time because the process is taking too long (this is especially common if you don't provide a progress meter or at least switch the mouse icon to an hourglass)

The consequences of this type of bug can range from annoying to catastrophic.  One common situation involves looping through recordsets and modifying database records.  Depending on the specifics, you can end up inserting duplicate records; generating a primary key violation that breaks a process halfway through; or deleting records that should not be deleted.

Necessary Conditions to Reproduce the Bug

As it turns out, I actually had some trouble creating sample code to demonstrate this bug pattern.  There are a few criteria that appear to be necessary to create the conditions for this bug:

  • A call to DoEvents (this allows Access to process the second click; without DoEvents, the single-threaded nature of VBA generally prevents this bug from appearing; however, keep in mind that DoEvents is often used to allow for the visual updating of a progress bar–which is more likely to be used with a long-running process)
  • A slow enough process that the user has time to double-click the button (I simulate this with the Sleep API in my example below)
  • A non-idempotent process (idempotent functions can safely be run multiple times)

It's a bit of an academic exercise, but I'm actually a bit torn on whether a process could be considered idempotent if it was subject to this kind of bug.  For example, I can easily imagine a procedure that could be run to completion as many times as you like, but that causes problems if it is interrupted partway through.  Would that be considered idempotent?

Minimal Complete and Verifiable Example

With the help of ChatGPT, I came up with a minimal example to demonstrate the nature of this bug:

  1. Create a form with three buttons:

    • btnNoGuardClause: with caption "No Guard Clause"
    • btnWithGuardClause: with caption "With Guard Clause"
    • btnResetCounters: with caption "Reset Counters"
  2. Add two text boxes:

    • tbClickCounter: with label captioned "Click counter:"
    • tbProcessCounter: with label captioned "Process counter:"
  3. Copy the code below into the form's code behind module:

Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Private Sub btnResetCounters_Click()
    Me.tbClickCounter = 0
    Me.tbProcessCounter = 0
End Sub

Private Sub btnNoGuardClause_Click()
    IncrementCounters
End Sub

Private Sub btnWithGuardClause_Click()
    Me.btnWithGuardClause.Enabled = False
    IncrementCounters
    Me.btnWithGuardClause.Enabled = True
End Sub


Private Sub IncrementCounters()
    ' Increment ClickCounter with each click
    Me.tbClickCounter = Me.tbClickCounter + 1
    
    ' Simulate a time-consuming process
    Dim CurrentValue As Integer
    CurrentValue = Me.tbProcessCounter
    
    Dim i As Long
    For i = 1 To 5
        Sleep 200
        DoEvents     'The DoEvents is what allows Access to process subsequent button clicks
    Next i
    
    ' Increment the ProcessCounter based on the previously read value
    Me.tbProcessCounter = CurrentValue + 1
End Sub
  1. Switch the form to Form View
  2. Click the [Reset Counters] button to set the two text boxes to 0
  3. Click the [No Guard Clause] button rapidly
    • The "Click counter" text box will be larger than the "Process counter" text box
  4. Click the [Reset Counters] button to set the two text boxes to 0
  5. Click the [With Guard Clause] button rapidly
    • The "Click counter" text box will increase in unison with the "Process counter" text box

While this example is obviously contrived, it demonstrates the potential for logic errors if you don't consider what might happen if your users double-click the command button for a long-running process.


Quick Tip: Preventing Accidental Double-Clicks
Pop quiz, hotshot: What would happen if a user double-clicked every button in your Access application? Would there be any unintended consequences?
Huh, apparently I wrote about this topic already. 🤦‍♂️
How to Safely Disable Controls in Microsoft Access
I *never* set the Enabled property of a form control directly. Instead, I use a convenience function so that I don’t have to worry about run-time error 2164.
It turns out my SetCtlEnabled() function is no longer needed in modern versions of Access.

Acknowledgements
  • Portions of this article's body generated with the help of ChatGPT
  • One or more code samples generated with the help of ChatGPT