🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

VBA Error Handling in Excel — The One Pattern Every Reliable Macro Uses

|

VBA Error Handling in Excel — The One Pattern Every Reliable Macro Uses

Tested on: Excel 365 v2509 · Excel 2021 · Excel 2019 · last verified 2026-06-08

TL;DR — VBA has no try/catch. Reliable error handling is one shape you reuse in every procedure that matters: arm a handler at the top, do the work, route every exit — success or failure — through a single cleanup point, then leave. Copy this template:

Sub ProcessReport()
    On Error GoTo ErrHandler
    Application.ScreenUpdating = False        ' state we MUST restore later

    Dim ws As Worksheet
    Set ws = Sheets("Data")                   ' any error here jumps to ErrHandler
    ws.Range("A1:A1000").Value = 0

CleanExit:                                    ' the ONE exit every path goes through
    Application.ScreenUpdating = True         ' always restored, error or not
    Exit Sub

ErrHandler:
    MsgBox "ProcessReport failed: " & Err.Description
    Resume CleanExit                          ' clean up, then leave
End Sub

That structure — On Error GoTo / work / CleanExit: / Exit Sub / ErrHandler: / Resume CleanExit — is the whole discipline. The rest of this guide is why each line is there.

The mental model: you're hand-building the try/finally VBA doesn't give you

Languages like Python and C# have try / except / finally. VBA has none of it. What it gives you instead is three primitives — On Error GoTo Label, a label, and Resume — and the expectation that you assemble them yourself into the equivalent structure.

Map it to try/finally and it stops looking arbitrary:

try/catch/finally VBA equivalent
try { On Error GoTo ErrHandler
catch (e) { ErrHandler: block, reading Err
finally { the CleanExit: block both paths reach
throw Err.Raise

Once you see the template as "manual try/finally," the placement of every line becomes obvious instead of memorised. The non-negotiable part is the single cleanup label that both the success path and the error path pass through.

The one rule: Resume decides where you go after the handler — pick wrong and you loop or leak

Inside a handler you have three ways to continue, and choosing the wrong one is the classic VBA error-handling bug:

Resume              ' RE-RUN the exact line that failed — only safe if you fixed the cause
Resume Next         ' SKIP the failed line, continue with the one after it
Resume CleanExit    ' JUMP to a label — normally your single cleanup exit
  • Resume retries the offending line. Use it only when the handler changed something so the retry can succeed (created a missing folder, opened a closed book). Use it blindly and you get an infinite loop — fail, handle, retry, fail, forever.
  • Resume Next continues past the failure. Fine for "log it and move on," dangerous if the skipped line was load-bearing.
  • Resume Label is the workhorse: it sends control to your CleanExit: so cleanup always runs before the procedure ends.

A handler that just falls off the end without any Resume/Exit lets the error effectively stop the macro — so always decide the exit explicitly.

The part everyone skips: the single cleanup point is what saves your file

Here's the failure mode that turns a small bug into a support ticket. A macro sets some global Excel state for speed:

Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual

…then errors out before turning them back on. Now Excel is frozen and won't recalculate, and the user thinks Excel itself is broken — long after your macro exited. The same story applies to an open file handle (left locked), a Set object (never released), or a sheet left unprotected.

The fix is structural, not careful coding: put every "must always run" line at a single label that both the normal path and the handler reach, and make the handler Resume to it:

Sub ImportFile()
    Dim ff As Integer
    On Error GoTo ErrHandler
    Application.ScreenUpdating = False
    ff = FreeFile
    Open "C:\Data\in.csv" For Input As #ff
    ' ... read the file, which might blow up halfway ...

CleanExit:
    If ff <> 0 Then Close #ff             ' file ALWAYS closed
    Application.ScreenUpdating = True      ' UI ALWAYS restored
    Exit Sub
ErrHandler:
    MsgBox "Import failed: " & Err.Description
    Resume CleanExit                      ' error path goes through cleanup too
End Sub

Success falls into CleanExit. Failure jumps to ErrHandler, then Resume CleanExit. Both routes close the file and restore the UI. That's your finally.

Bubbling errors up to the caller (re-raising)

A low-level helper usually shouldn't decide how the whole app reacts — it should clean up its own resources and let the caller decide. Capture the error, restore state, then re-raise:

Private Sub LoadData()
    On Error GoTo ErrHandler
    ' ... work that may fail ...
    Exit Sub
ErrHandler:
    Dim n As Long, d As String, s As String
    n = Err.Number: d = Err.Description: s = Err.Source   ' save before cleanup wipes Err
    ' (local cleanup here)
    Err.Raise n, s, d                                     ' re-throw to the caller's handler
End Sub

Save the Err values first — cleanup statements clear Err out from under you. Then Err.Raise sends the original error up to whoever called LoadData, where a top-level handler can log it once and show the user one clean message.

A central logging handler

For anything you ship, log errors somewhere you can read later instead of trusting the user to screenshot a MsgBox:

ErrHandler:
    LogError "ProcessReport", Err.Number, Err.Description
    Resume CleanExit
End Sub

Private Sub LogError(proc As String, num As Long, desc As String)
    Dim ff As Integer: ff = FreeFile
    Open ThisWorkbook.Path & "\error_log.txt" For Append As #ff
    Print #ff, Now & " | " & proc & " | " & num & " | " & desc
    Close #ff
End Sub

When On Error Resume Next is the right tool

Not every situation wants a handler. For one line where failure is expected and harmless — probing whether an object exists — narrow Resume Next is cleaner than a label, as long as you check Err.Number and disarm with On Error GoTo 0:

On Error Resume Next
Set ws = Sheets("Optional")
On Error GoTo 0
If ws Is Nothing Then Set ws = Sheets.Add   ' didn't exist → create it

The full rundown of when this is safe vs. reckless is in On Error: Resume Next vs GoTo.

The opinion: maturity isn't how many handlers you write — it's whether they converge

You can sprinkle On Error GoTo over every procedure and still write fragile macros. The signal of error-handling maturity is narrower and harsher: does every procedure have one exit that all paths pass through, where cleanup happens? If the answer is no, then a single mid-procedure error leaves ScreenUpdating off, the calc engine manual, and a file locked — and your user blames Excel, not your code.

So my bar for "this macro is production-ready" isn't the presence of handlers. It's: pick any line, imagine it throwing — does the file still get closed and the UI still get restored? If yes, you've internalised the pattern. If no, more On Error statements won't save you; a CleanExit: will.

Common error-handling mistakes (and the fix)

Symptom Cause Fix
Excel "freezes" after a macro error ScreenUpdating/Calculation never restored Restore them at a CleanExit: the handler Resumes to
File stays locked / "already open" Close/Set …= Nothing skipped on the error path Put cleanup at the single exit both paths reach
Infinite loop Resume retries a line whose cause wasn't fixed Use Resume Next or Resume Label, not bare Resume
Caller never learns it failed Helper swallowed the error with a MsgBox Save Err, clean up, then Err.Raise to re-throw
Log shows error 0 Read Err after cleanup cleared it Copy Err.Number/Description before cleaning up
Success shows the failure message too No Exit Sub before ErrHandler: Put Exit Sub (or fall into CleanExit) before the label

When the cleanup outweighs the work — describe the job instead

Look back at that template. The actual task is two lines; the handler, the cleanup label, the Resume, the logging, the re-raise are the other twenty. That ratio only gets worse on real pipelines. ExcelMaster Agent lets you describe the job — "import these CSVs, match on order ID, flag mismatches, email finance" — in plain English, and the generated Python handles failures, restores state, and backs up your workbook before it touches a thing. No On Error, no Resume, no frozen Excel. Try it free →

FAQ

Does VBA have try/catch? No. VBA has no try/catch/finally. You build the equivalent from On Error GoTo Label, a handler label, a single cleanup label, and Resume. The standard template arms a handler at the top, routes every exit through one cleanup point, then leaves.

What is the best error handling pattern in VBA? Arm On Error GoTo ErrHandler, do the work, then have a CleanExit: label where you restore state (ScreenUpdating, open files, objects) and Exit Sub. The ErrHandler: block reads Err, optionally logs it, and Resume CleanExit. Both success and failure pass through the same cleanup — that's the key.

What is the difference between Resume, Resume Next, and Resume Label? Resume re-runs the line that failed (only safe if the handler fixed the cause, or you get an infinite loop). Resume Next skips the failed line and continues. Resume Label jumps to a label — usually your single cleanup exit so cleanup always runs.

How do I make sure cleanup always runs after an error in VBA? Put every "must always run" statement at one label (e.g. CleanExit:) placed so the normal path falls into it, and make the error handler Resume CleanExit. Both paths then execute the cleanup. This is VBA's substitute for a finally block.

How do I pass an error from a helper up to the calling procedure? In the helper's handler, save Err.Number, Err.Description, and Err.Source into variables, do any local cleanup, then call Err.Raise with those saved values. The error propagates to the caller's On Error handler, where you can log it once and show the user a single message.