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
Resumeretries 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 Nextcontinues past the failure. Fine for "log it and move on," dangerous if the skipped line was load-bearing.Resume Labelis the workhorse: it sends control to yourCleanExit: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 →
Related guides
- VBA On Error — Resume Next vs GoTo & Why Macros Hide Bugs
- VBA Err Object — Number, Description & Raising Your Own Errors
- VBA Sub in Excel — Procedures, Calling & Why Your Macro Is Just a Sub
- VBA For Loop in Excel — 8 Real-World Examples
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.
