February 15, 2024

VBA run-time error handling mechanism

Last modified: Wed Mar 13 20:26:30 UTC+0100 2024 © Attila Tarpai

VBA error handling mechanism

Err.Raise() starts the same process as if a Run-time error has occurred:

  • On Run-time error or Err.Raise() VBA looks for an error handler along the call stack starting with the current procedure.
  • If none of the procedure has an error handler, the error bubbles up to the default error handler: a modal error message is displayed and execution stops.
  • If an error handler is defined in the current or closest caller, the error is trapped there.
  • Run-time errors or Err.Raise() occurring in the error handler bubbles up again i.e. error trapping is disabled when executing the error handler (to avoid infinite loop).
  • After execution transferred to another handler in another procedure, it never returns. Resume is local to the procedure.
   bubble
     ^      VBA Run-time
     |        | On Error.. error message is displayed
     |        |            and execution stops (default)
     |        |
              |__Sub1()
     ^            | On Error.. error handler in procedure
     |            |
                  |__Sub2()
     ^                |
     |                |
                      |__Sub3()
     ^                    | On Error.. error handler in procedure
     |                    |
                          |__Sub4()
     ^                        |
     |                        |
                              |

 Run-time error or Err.Raise in Sub4() is trapped in Sub3()
 Run-time error or Err.Raise in Sub3() is trapped in Sub3()
 Run-time error or Err.Raise in Sub3()'s error handler is trapped in Sub1()

About Line numbers vs Line labels

It seems like, at least in 64-bit Excel for Microsoft 365, there is not much difference any longer between Line numbers and Line labels.

Here "999" is Line number and "999:" is Line label:

Sub errtrap()                         Sub errtrap()
On Error GoTo 999                     On Error GoTo 999

      Err.Raise 3                           Err.Raise 3
      Exit Sub                              Exit Sub

999                                   999:
      Resume Next                           Resume Next
End Sub                               End Sub

These are not really true in VBA Docs either:

  • Line numbers should be unique within the module - NOT. Only within procedure.
  • Line labels should start with a letter - NOT. See the above compiles and runs.

At least using both "999" and "999:" within the same procedure gives Duplicate declaration error.

There is a little difference between Line numbers and Line labels when it comes to F8-debugging step-by-step:

  • Empty line with only a label is a F8-Step line.
  • Empty line with only a Line number is not: F8 steps on the next non-empty code line right away.

VBA Error handler Execution model

Custom run-time error handlers in VBA are implemented by jumping around within the procedure set up and controlled by Line numbers/labels, On Error and Resume statements. It's a bit like try...catch blocks but with more freedom.

Next is a pseudo-implementation model I have come up to understand what may happen internally.

Lets say every procedure has an Error Handling Mode internal variable. The On Error statement sets this variable to 3 possible values, which will go into effect on the next Run-time error or Err.Raise():

On Error GoTo Line    ---> sets to [Line]
On Error Resume Next  ---> sets to [Next]
On Error GoTo 0       ---> sets to [0]

By default On Error GoTo 0 is in effect i.e. error trapping is disabled. Run-time errors in the procedure will bubble and eventually handled above.

There is also a Code Execution State of the current line being executed: normal or trapped. Trapped mode [T] is entered only after On Error GoTo Line is in effect and a Run-time error occurs or the Err.Raise() statement issued. Execution jumps to Line and Trapped mode is activated.

  Code                                Error
 Execution                           Handling
  State                               Mode

----- Sub() ---------------------------------+
                              [0][Line][Next]|       bubble [0]
  [ ]                                        |         ^
  [ ]       ..                               |         |
  [ ]       Raise                            | jump ---+---> [Next]
  [ ]       next-statement                   |         |
  [ ]       ...                              |         V
  [ ]                                        |       [Line]
  [ ]                                        |
  [T] Line  +--[enter]---------------+       |
  [T]       |                        |       |
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |       bubble
  [T]       |  ..                    |       |         ^
  [T]       |  ..                    |       |         |
  [T]       |  Raise (no return)     |       | jump ---+
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |
  [T]       |  ..                    |       |
  [T]       |                        |       |         +---> Re-try statement
  [T]       |  Resume                |       | jump ---+---> next-statement
  [T]       +--[exit][jump]----------+       |         +---> line
                                             |
                                             |
  [T]       |                        |       |
  [T]       |  On Error GoTo -1      |       |
  [T]       +--[exit]----------------+       |
  [ ]          ..                            |
  [ ]          ..                            |
  [ ]          ..                            |
---------------------------------------------+


  [T]         denotes lines executed in error trapped mode
  [ ]         are normal execution code lines
  [enter]     set [T] AND ignore Error Handling Mode on next error
  [exit]      clear [T] AND honour Error Handling Mode on next error

The main difference in trapped mode [T] is:

  • The Error Handling Mode is ignored, disabled. Run-time error or Err.Raise() will bubble. Even if an On Error statement appears in the error handler, which is legal and changes the Error Handling Mode.
  • Resume is a valid statement

There are 2 ways to exit trapped mode:

  • Resume, which always jumps
  • On Error GoTo -1

On Error GoTo -1

It does not change the Error Handling Mode and useful only in trapped mode (in normal execution mode it only clears the ErrObject):

  • clears the ErrObject
  • clears the [T] flag
  • continues execution on the next line in normal execution mode

BEWARE: errors now will cause endless loop if Error Handling Mode hasn't change from [Line].

On Error GoTo -1 is the same as Resume without jump - i.e. jump to the next line.

These are equivalent:

Sub errtrap()                           Sub errtrap()
  On Error GoTo label                     On Error GoTo label

      ...                                     ...
      Exit Sub                                Exit Sub

label:                                  label:
      ...                                     ...
      On Error GoTo -1                        Resume out
      ...                               out:  ...
      ...                                     ...
End Sub                                 End Sub

On Error Resume Next

When On Error Resume Next is set and an error occurs code doesn't enter trapped mode:

  • fill ErrObject
  • execute next statement
  • ErrObject retains its values until Clear

Resume

Resume
Resume Next
Resume Line

Resume can only be executed in trapped mode [T] (otherwise 20, Resume without error).

  • clears the ErrObject
  • clears the [T] flag
  • transfers execution back to the same, next or specified line in normal execution mode

Resume jump is always local to the procedure, it never jumps "back" to a nested procedure that caused the error.

Behaviour of the ErrObject

Every On Error and Resume statement clears the Err Object.

A Run-time error or Err.Raise() statement fills values in the ErrObject. The only difference I have found is:

  • Repeated Err.Raise() on a not cleared ErrObject sets only ErrNum.
  • Repeated internal Run-time errors update all values in the ErrObject.

Infinite loops with trapped code

It is a danger to end up in infinite loops using error handlers and issuing certain On Error/Resume statements.

InfiniteLoop1()
Resume in the error handler re-tries the Err.Raise 3 statement, which is trapped again...

Sub InfiniteLoop1()
On Error GoTo 1
    Err.Raise 3
    Exit Sub        ' never reached!
1
    Resume
End Sub

InfiniteLoop2()
On Error GoTo -1 clears the [T] flag and the next Err.Raise() is trapped again...

Sub InfiniteLoop2()
On Error GoTo 1
    Err.Raise 3
    Exit Sub        ' never reached!
1
    On Error GoTo -1
    Err.Raise 11
    Resume Next     ' never reached!
End Sub

InfiniteLoop3()
On Error GoTo -1 clears the [T] flag, which causes Resume an invalid statement, which is trapped again...

Sub InfiniteLoop3()
On Error GoTo 1
    Err.Raise 3
    Exit Sub        ' never reached!
1
    On Error GoTo -1
    Resume Next     ' Run-time error 20, Resume without error
                    ' next line never reached!
End Sub




No comments:

Post a Comment