Unstructured Error Handling: On Error

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors (except where the page says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The root namespace of the scratch projects is ShelfDocs. Compiler diagnostics are shown with the file name Program.vb. The error examples are separate failing programs.

On Error is the error-handling model of Visual Basic 6 and VBA, kept in the language so that old code compiles. It is the only page of this section on which On Error appears in examples. New Visual Basic code uses Structured Exception Handling (Try, Catch, Finally), and Microsoft’s documentation says so too. The two models are not separate runtimes. Both run on .NET exceptions: an On Error handler is entered when an exception is thrown, and Err is a view of that exception. What differs is the control flow, a procedure-wide handler with GoTo and Resume instead of blocks, and the rules that follow from it. Read this page to understand legacy code (see Migrating Legacy Visual Basic and The VB Family: VB6, VBA and VBScript) and to migrate it.

Statement Effect

On Error GoTo label

enables a handler: when an error occurs in this procedure, control jumps to label

On Error Resume Next

enables "ignore and continue": after an error, execution continues at the next statement; the error is only recorded in Err

On Error GoTo 0

disables the enabled handler in this procedure and clears Err

On Error GoTo -1

clears the error and disables the handler, used inside a handler to leave the "active" state

Resume

in a handler: retry the statement that failed

Resume Next

in a handler: continue with the statement after the one that failed

Resume label

in a handler: continue at a label

Err

the ErrObject describing the most recent error: Number, Description, Source, Raise, Clear, GetException

Error number

the statement that raises the run-time error with that VB6 number (legacy form of Err.Raise)

Erl

the line number of the last numbered line executed before the error

How the model works

A procedure has at most one enabled handler at a time; On Error is an executable statement that replaces it. An error that occurs while the handler is enabled transfers control to the label (the handler becomes active) or, for Resume Next, to the next statement. A handler is not a Sub: it is a section of the same procedure, reached by a jump and left by Resume, Exit Sub, Return, End Sub or another On Error. An error raised while a handler is active is not handled by the same procedure; it goes to the caller.

flowchart TD A(["A statement raises an exception"]) --> B{"Handler enabled in this procedure?"} B -->|"none, or handler is active"| C["Propagate to the caller, which repeats this test"] B -->|"On Error Resume Next"| D["Record the error in Err and continue at the next statement"] B -->|"On Error GoTo label"| E["Jump to the label: the handler is now active"] E --> F{"How does the handler end?"} F -->|"Resume"| G["Retry the failing statement"] F -->|"Resume Next"| H["Continue after the failing statement"] F -->|"Resume label"| I["Continue at that label"] F -->|"Exit Sub, Return, End Sub"| J["Leave the procedure"]

On Error GoTo label

The usual shape: enable the handler at the top, write the normal path, leave with Return (or Exit Sub/Exit Function) before the handler label, and write the handler after it. The handler reads the Err object before doing anything that could replace it.

Option Strict On

Module Program
    Private Function SafeDivide(numerator As Integer, denominator As Integer) As Integer
        On Error GoTo Handler
        Dim result As Integer = numerator \ denominator
        Return result
Handler:
        Console.WriteLine("  Err.Number = " & Err.Number & ", Err.Description = " & Err.Description)
        Console.WriteLine("  Err.Source = " & Err.Source)
        Return -1
    End Function

    Private Sub NoExitBeforeHandler()
        On Error GoTo Oops
        Console.WriteLine("  body ran fine")
Oops:
        Console.WriteLine("  handler code also runs, Err.Number = " & Err.Number)
    End Sub

    Sub Main()
        Console.WriteLine("10 \ 2:")
        Console.WriteLine("  -> " & SafeDivide(10, 2))
        Console.WriteLine("10 \ 0:")
        Console.WriteLine("  -> " & SafeDivide(10, 0))
        Console.WriteLine("Falling into the label without an Exit:")
        NoExitBeforeHandler()
    End Sub
End Module
10 \ 2:
  -> 5
10 \ 0:
  Err.Number = 11, Err.Description = Attempted to divide by zero.
  Err.Source = System.Private.CoreLib
  -> -1
Falling into the label without an Exit:
  body ran fine
  handler code also runs, Err.Number = 0
  • 10 \ 0 raised a DivideByZeroException; Err.Number is the VB6 number for it (11), Err.Description is the exception’s message, and Err.Source is the assembly that threw it (System.Private.CoreLib for a runtime exception).

  • The label is a line label in the same procedure, followed by a colon. A label must exist (BC30132 otherwise).

  • If the code does not Return or Exit before the label, the handler’s statements run on the normal path too (the last case: Err.Number is 0 because nothing failed). Forgetting the Exit is the classic bug of this model.

On Error Resume Next, Resume and Resume Next

On Error Resume Next makes the procedure keep going. The failing statement is abandoned (assignments are not made), the error is recorded in Err, and execution continues at the next statement. It is "inline" error handling: the code tests Err.Number after the statements that might fail and calls Err.Clear(). Inside a handler entered with On Error GoTo, the three Resume forms decide where to continue.

Option Strict On

Module Program
    Private Sub ResumeNextDemo()
        Dim values() As String = {"4", "x", "7"}
        Dim total As Integer = 0
        Dim i As Integer
        On Error Resume Next
        For i = 0 To values.Length - 1
            total += Integer.Parse(values(i))
            If Err.Number <> 0 Then
                Console.WriteLine("  skipped '" & values(i) & "': Err.Number " & Err.Number)
                Err.Clear()
            End If
        Next
        Console.WriteLine("  total = " & total)
    End Sub

    Private Sub ResumeNextInHandler()
        On Error GoTo Handler
        Console.WriteLine("  before")
        Dim zero As Integer = 0
        Console.WriteLine("  value = " & (5 \ zero))
        Console.WriteLine("  after the failing statement")
        Return
Handler:
        Console.WriteLine("  handler: " & Err.Description)
        Resume Next
    End Sub

    Private Sub ResumeRetry()
        Dim attempts As Integer = 0
        Dim divisor As Integer = 0
        On Error GoTo Fix
        Console.WriteLine("  7 \ divisor = " & (7 \ divisor))
        Return
Fix:
        attempts += 1
        Console.WriteLine("  attempt " & attempts & " failed (" & Err.Description & "), changing divisor to 2")
        divisor = 2
        Resume
    End Sub

    Private Sub ResumeLabel()
        On Error GoTo Handler
        Dim zero As Integer = 0
        Console.WriteLine("  value = " & (5 \ zero))
        Console.WriteLine("  skipped by Resume Done")
Done:
        Console.WriteLine("  at Done")
        Return
Handler:
        Console.WriteLine("  handler: " & Err.Number)
        Resume Done
    End Sub

    Sub Main()
        Console.WriteLine("On Error Resume Next:")
        ResumeNextDemo()
        Console.WriteLine("Resume Next in a handler:")
        ResumeNextInHandler()
        Console.WriteLine("Resume (retry the statement):")
        ResumeRetry()
        Console.WriteLine("Resume label:")
        ResumeLabel()
    End Sub
End Module
On Error Resume Next:
  skipped 'x': Err.Number 13
  total = 11
Resume Next in a handler:
  before
  handler: Attempted to divide by zero.
  after the failing statement
Resume (retry the statement):
  attempt 1 failed (Attempted to divide by zero.), changing divisor to 2
  7 \ divisor = 3
Resume label:
  handler: 11
  at Done
  • Resume retries the statement that failed; it loops forever if the handler does not fix the cause. The example repairs divisor first.

  • Resume Next continues after the failing statement. If the failure was in a called procedure with no handler of its own, "the failing statement" is the call, as the procedure-call example below shows.

  • Resume label jumps to a label, and may leave the handler for a shared clean-up path.

  • Resume belongs in a handler. It compiles anywhere in a procedure that has On Error, but fails at run time when no error is being handled (see "Resume without error" below).

  • A Resume Next without a following check is the opposite of error handling: it hides every failure in the procedure. Prefer TryParse-style APIs and explicit checks.

On Error GoTo 0 and On Error GoTo -1

On Error GoTo 0 switches handling off: the next error is no longer trapped by this procedure and travels up to the caller (and Err is cleared). On Error GoTo -1 clears the error and disables the handler; it is the way to leave the active state inside a handler so that a later On Error GoTo can trap a second error. An error that occurs while a handler is active also escapes to the caller, as the last case shows.

Option Strict On

Module Program
    Private Sub Fail(label As String)
        Throw New InvalidOperationException(label)
    End Sub

    Private Sub HandlerOff()
        On Error GoTo Handler
        Console.WriteLine("  error 1 is handled")
        Fail("one")
Handler:
        Console.WriteLine("  handler: " & Err.Description)
    End Sub

    Private Sub GoToZero()
        On Error Resume Next
        Fail("first, ignored")
        Console.WriteLine("  after first failure, Err.Number = " & Err.Number)
        On Error GoTo 0
        Console.WriteLine("  handling is off now; Err.Number is still " & Err.Number)
        Fail("second, not handled here")
        Console.WriteLine("  never printed")
    End Sub

    Private Sub GoToMinusOne()
        Dim step1 As Integer = 0
        On Error GoTo First
        Fail("a")
        Return
First:
        step1 += 1
        Console.WriteLine("  First handler, Err.Number = " & Err.Number)
        On Error GoTo -1                  ' clears the error and the active handler
        Console.WriteLine("  after GoTo -1, Err.Number = " & Err.Number)
        On Error GoTo Second
        Fail("b")
        Return
Second:
        Console.WriteLine("  Second handler: " & Err.Description & " (step1 = " & step1 & ")")
    End Sub

    Private Sub ErrorInsideHandler()
        On Error GoTo Handler
        Fail("outer")
        Return
Handler:
        Console.WriteLine("  in handler, about to fail again")
        Fail("raised inside the handler")
    End Sub

    Private Sub Run(name As String, action As Action)
        Console.WriteLine(name & ":")
        Try
            action()
        Catch ex As Exception
            Console.WriteLine("  escaped to the caller: " & ex.GetType().Name & " - " & ex.Message)
        End Try
    End Sub

    Sub Main()
        Run("handler reached by falling through", AddressOf HandlerOff)
        Run("On Error GoTo 0", AddressOf GoToZero)
        Run("On Error GoTo -1", AddressOf GoToMinusOne)
        Run("error inside a handler", AddressOf ErrorInsideHandler)
    End Sub
End Module
handler reached by falling through:
  error 1 is handled
  handler: one
On Error GoTo 0:
  after first failure, Err.Number = 5
  handling is off now; Err.Number is still 0
  escaped to the caller: InvalidOperationException - second, not handled here
On Error GoTo -1:
  First handler, Err.Number = 5
  after GoTo -1, Err.Number = 0
  Second handler: b (step1 = 1)
error inside a handler:
  in handler, about to fail again
  escaped to the caller: InvalidOperationException - raised inside the handler

Handlers and procedure calls

Handlers are per procedure. An error raised in a procedure with no enabled handler unwinds to the caller, and each caller on the way is asked in turn. When a caller with an enabled, inactive handler is found, that handler runs; Resume Next there continues after the call that failed, in the caller. On Error Resume Next is not inherited by callees: each procedure that wants inline handling needs its own statement, and if the callee has none the error returns to the caller as shown. With no handler anywhere, the exception is unhandled and ends the process (see the global handlers on the structured page).

Option Strict On

Module Program
    Private Sub Level3()
        Console.WriteLine("  Level3: about to fail")
        Dim zero As Integer = 0
        Console.WriteLine(1 \ zero)
        Console.WriteLine("  Level3: never printed")
    End Sub

    Private Sub Level2()
        Console.WriteLine("  Level2: no handler here")
        Level3()
        Console.WriteLine("  Level2: never printed")
    End Sub

    Private Sub Level1()
        On Error GoTo Handler
        Console.WriteLine("  Level1: calls Level2")
        Level2()
        Console.WriteLine("  Level1: runs after Resume Next (the line after the failed call)")
        Return
Handler:
        Console.WriteLine("  Level1 handler: " & Err.Description)
        Resume Next
    End Sub

    Private Sub Level1Ignoring()
        On Error Resume Next
        Level2()
        Console.WriteLine("  Level1Ignoring: continues after the call that failed, Err.Number = " & Err.Number)
    End Sub

    Sub Main()
        Level1()
        Console.WriteLine("---")
        Level1Ignoring()
    End Sub
End Module
  Level1: calls Level2
  Level2: no handler here
  Level3: about to fail
  Level1 handler: Attempted to divide by zero.
  Level1: runs after Resume Next (the line after the failed call)
---
  Level2: no handler here
  Level3: about to fail
  Level1Ignoring: continues after the call that failed, Err.Number = 11

The Err object

Err is a global function (in Microsoft.VisualBasic.Information) that returns the ErrObject holding the most recent error of the current thread. ErrObject is a thin view of a .NET exception: setting up an Err.Raise creates an exception, and an exception caught by a handler (or by Try) populates Err.

Member Meaning

Number

the VB6 error number; 0 means no error

Description

the message (the exception’s Message, or the text given to Err.Raise)

Source

the assembly (or application) that raised the error

HelpFile, HelpContext

VB6 help integration; empty and 0 for .NET exceptions

LastDllError

the last error code from a `Declare`d Windows call; 0 here (see Native Interop)

Clear()

resets all properties; use it after handling an error with Resume Next

Raise(Number, Source, Description, HelpFile, HelpContext)

raises a run-time error

GetException()

the .NET Exception object behind the error; use it to branch on the real type

The next program raises the .NET exceptions a legacy program meets most often and prints the number that Err.Number takes, with the HResult that the exception itself reports. On Error Resume Next is used so that the loop continues; Err.GetException() gives the exception behind the number.

Option Strict On

Module Program
    Private Sub Raise(kind As Integer)
        Select Case kind
            Case 1 : Throw New DivideByZeroException()
            Case 2 : Throw New OverflowException()
            Case 3 : Throw New InvalidCastException()
            Case 4 : Throw New IndexOutOfRangeException()
            Case 5 : Throw New ArgumentException("bad argument")
            Case 6 : Throw New NullReferenceException()
            Case 7 : Throw New OutOfMemoryException()
            Case 8 : Throw New IO.FileNotFoundException()
            Case 9 : Throw New IO.DirectoryNotFoundException()
            Case 10 : Throw New FormatException()
            Case 11 : Throw New InvalidOperationException()
            Case 12 : Throw New NotSupportedException()
            Case 13 : Throw New TimeoutException()
            Case 14 : Throw New UnauthorizedAccessException()
            Case Else : Throw New ApplicationException("custom")
        End Select
    End Sub

    Sub Main()
        Console.WriteLine("Exception type                  Err.Number  HResult")
        For kind As Integer = 1 To 15
            On Error Resume Next
            Err.Clear()
            Raise(kind)
            Dim ex As Exception = Err.GetException()
            Console.WriteLine(ex.GetType().Name.PadRight(32) & Err.Number.ToString().PadRight(12) & "0x" & ex.HResult.ToString("X8"))
        Next
    End Sub
End Module
Exception type                  Err.Number  HResult
DivideByZeroException           11          0x80020012
OverflowException               6           0x80131516
InvalidCastException            13          0x80004002
IndexOutOfRangeException        9           0x80131508
ArgumentException               5           0x80070057
NullReferenceException          91          0x80004003
OutOfMemoryException            7           0x8007000E
FileNotFoundException           53          0x80070002
DirectoryNotFoundException      76          0x80070003
FormatException                 13          0x80131537
InvalidOperationException       5           0x80131509
NotSupportedException           13          0x80131515
TimeoutException                5           0x80131505
UnauthorizedAccessException     5           0x80070005
ApplicationException            5           0x80131600

Source: ErrObject class

Reading the table: runtime exceptions with a classic VB6 meaning map to that number (DivideByZeroException is 11, OverflowException 6, InvalidCastException 13, IndexOutOfRangeException 9, NullReferenceException 91, OutOfMemoryException 7, FileNotFoundException 53, DirectoryNotFoundException 76). Some .NET exceptions map to 13 (FormatException, NotSupportedException) and anything without a specific mapping, including every custom exception and InvalidOperationException, comes out as 5, "Invalid procedure call or argument". So Err.Number cannot tell two exceptions apart reliably; use Err.GetException() and test its type, or move to Try/Catch, which tests the type directly.

Err.Raise and the Error statement

Err.Raise(number, source, description) raises an error. VB6 numbers that mean something are turned into the matching .NET exception (5 becomes an ArgumentException, 11 a DivideByZeroException, 53 a FileNotFoundException); other numbers become a plain Exception whose message is the description. Custom errors conventionally use vbObjectError + n. The old Error 11 statement does the same as Err.Raise(11), with the standard VB6 text. A Try ... Catch block also fills Err, so legacy helper code that reads Err.Number works inside a Catch.

Option Strict On

Module Program
    Private Sub RaiseDemo(number As Integer)
        On Error GoTo Handler
        Err.Raise(number, "ShelfDocs", "raised with number " & number)
        Return
Handler:
        Dim ex As Exception = Err.GetException()
        Console.WriteLine("  Err.Raise(" & number & ") -> " & ex.GetType().Name & " | Number=" & Err.Number & " | Source=" & Err.Source & " | " & Err.Description)
    End Sub

    Private Sub ErrorStatementDemo()
        On Error GoTo Handler
        Error 11
        Return
Handler:
        Console.WriteLine("  Error 11 -> " & Err.GetException().GetType().Name & ": " & Err.Description)
    End Sub

    Private Sub TryCatchSetsErr()
        Try
            Throw New IO.FileNotFoundException("catalog.json")
        Catch ex As Exception
            Console.WriteLine("  inside Catch: Err.Number = " & Err.Number & ", Err.Description = " & Err.Description)
        End Try
    End Sub

    Sub Main()
        Console.WriteLine("Err.Raise:")
        RaiseDemo(5)
        RaiseDemo(11)
        RaiseDemo(53)
        RaiseDemo(1000)
        RaiseDemo(vbObjectError + 513)
        Console.WriteLine("Error statement:")
        ErrorStatementDemo()
        Console.WriteLine("Try ... Catch also fills Err:")
        TryCatchSetsErr()
        Console.WriteLine("vbObjectError = " & vbObjectError)
    End Sub
End Module
Err.Raise:
  Err.Raise(5) -> ArgumentException | Number=5 | Source=ShelfDocs | raised with number 5
  Err.Raise(11) -> DivideByZeroException | Number=11 | Source=ShelfDocs | raised with number 11
  Err.Raise(53) -> FileNotFoundException | Number=53 | Source=ShelfDocs | raised with number 53
  Err.Raise(1000) -> Exception | Number=1000 | Source=ShelfDocs | raised with number 1000
  Err.Raise(-2147220991) -> Exception | Number=-2147220991 | Source=ShelfDocs | raised with number -2147220991
Error statement:
  Error 11 -> DivideByZeroException: Division by zero.
Try ... Catch also fills Err:
  inside Catch: Err.Number = 53, Err.Description = catalog.json
vbObjectError = -2147221504

Source: Error Statement

Erl and line numbers

Erl() returns the line number of the most recent numbered line that ran before the error. Numbered lines are the VB6 way to locate an error; modern code gets the line from the stack trace instead (ex.StackTrace or the logger). Without numbered lines Erl() is 0.

Option Strict On

Module Program
    Private Sub Numbered()
        Dim zero As Integer = 0
        On Error GoTo Handler
10:     Console.WriteLine("  line 10 ran")
20:     Console.WriteLine("  line 20 divides: " & (1 \ zero))
30:     Console.WriteLine("  line 30 never ran")
        Return
Handler:
        Console.WriteLine("  Erl = " & Erl() & ", Err.Number = " & Err.Number)
        Console.WriteLine("  HelpFile = '" & Err.HelpFile & "', HelpContext = " & Err.HelpContext & ", LastDllError = " & Err.LastDllError)
    End Sub

    Private Sub Unnumbered()
        Dim zero As Integer = 0
        On Error GoTo Handler
        Console.WriteLine(1 \ zero)
        Return
Handler:
        Console.WriteLine("  Erl without line numbers = " & Erl())
    End Sub

    Sub Main()
        Console.WriteLine("Numbered lines:")
        Numbered()
        Console.WriteLine("Unnumbered:")
        Unnumbered()
    End Sub
End Module
Numbered lines:
  line 10 ran
  Erl = 20, Err.Number = 11
  HelpFile = '', HelpContext = 0, LastDllError = 0
Unnumbered:
  Erl without line numbers = 0

Resume without an error

Resume and Resume Next can run only while an error is being handled. Reaching one on the normal path raises run-time error 20, "Resume without error". In the first procedure below the label is reached only by the Exit Sub jump over it, so it is never entered; in the second, a Resume Next statement sits on the normal path and fails:

Option Strict On

Module Program
    Private Sub ResumeInsideHandler()
        On Error GoTo Handler
        Console.WriteLine("  no error happens")
        Exit Sub
Handler:
        Resume Next
    End Sub

    Private Sub ResumeOnTheNormalPath()
        On Error GoTo Handler
        Resume Next
        Exit Sub
Handler:
        Console.WriteLine("  Err.Number " & Err.Number & ": " & Err.Description)
    End Sub

    Sub Main()
        Console.WriteLine("Resume only inside a handler that ran for an error:")
        ResumeInsideHandler()
        Console.WriteLine("Resume on the normal path:")
        ResumeOnTheNormalPath()
    End Sub
End Module
Resume only inside a handler that ran for an error:
  no error happens
Resume on the normal path:
  Err.Number 20: Resume without error.

Rules and compiler errors

The structural rules of the model are enforced by the compiler. These are the errors the 17.13 compiler printed for each broken rule.

Do not mix with Try. A procedure can use Try or On Error, not both (BC30544, reported on both statements):

Option Strict On

Module Program
    Sub Main()
        On Error GoTo Handler
        Try
            Console.WriteLine("a")
        Catch ex As Exception
        End Try
Handler:
    End Sub
End Module
Program.vb(5,9): error BC30544: Method cannot contain both a 'Try' statement and an 'On Error' or 'Resume' statement.
Program.vb(6,9): error BC30544: Method cannot contain both a 'Try' statement and an 'On Error' or 'Resume' statement.

Labels and special procedures. The target of On Error GoTo must be a label that exists (a plain number is a label, so GoTo 5 fails the same way), and On Error and Resume are not allowed in Async methods or iterators (BC36956):

Option Strict On

Module Program
    Sub Main()
        On Error GoTo Missing
        Console.WriteLine("a")
    End Sub
    Async Function RunAsync() As Task
        On Error Resume Next
        Await Task.Delay(1)
    End Function
    Iterator Function Numbers() As IEnumerable(Of Integer)
        On Error Resume Next
        Yield 1
    End Function
End Module
Program.vb(5,23): error BC30132: Label 'Missing' is not defined.
Program.vb(9,9): error BC36956: 'On Error' and 'Resume' cannot appear inside async or iterator methods.
Program.vb(13,9): error BC36956: 'On Error' and 'Resume' cannot appear inside async or iterator methods.
Option Strict On

Module Program
    Sub Main()
        On Error GoTo 5
        Resume Next
    End Sub
End Module
Program.vb(5,23): error BC30132: Label '5' is not defined.

Not inside a lambda. On Error and Resume cannot be written inside the body of a lambda expression (BC36668), but a method that contains a lambda, a query or a Using statement can still have its own On Error handler:

Option Strict On

Module Program
    Sub Main()
        Dim safe As Action = Sub()
                                 On Error Resume Next
                                 Console.WriteLine("x")
                             End Sub
        safe()
    End Sub
End Module
Program.vb(6,34): error BC36668: 'On Error' and 'Resume' cannot appear inside a lambda expression.

What changed from the older rules

Older documentation, and the Learn pages for BC36595 and BC36013, say that a procedure with On Error cannot also contain a lambda expression, a query expression or a Using statement. The 17.13 compiler on .NET 10 accepts all three, and the handler traps errors raised in the code around them, including errors in the Using body (after the resource has been disposed), in a lambda that the method calls, and in the statements after a query. This program, with On Error GoTo in each method, built without a warning:

Option Strict On

Public Class Tracker
    Implements IDisposable

    Public Sub Dispose() Implements IDisposable.Dispose
        Console.WriteLine("  Tracker disposed")
    End Sub
End Class

Module Program
    Private Sub WithUsing()
        On Error GoTo Handler
        Using t As New Tracker()
            Console.WriteLine("  inside Using")
            Dim zero As Integer = 0
            Console.WriteLine(1 \ zero)
        End Using
        Return
Handler:
        Console.WriteLine("  handler: " & Err.Description)
    End Sub

    Private Sub WithLambda()
        On Error GoTo Handler
        Dim twice As Func(Of Integer, Integer) = Function(x) x * 2
        Console.WriteLine("  twice(4) = " & twice(4))
        Dim zero As Integer = 0
        Console.WriteLine(1 \ zero)
        Return
Handler:
        Console.WriteLine("  handler: " & Err.Description)
    End Sub

    Private Sub WithQuery()
        On Error GoTo Handler
        Dim numbers() As Integer = {1, 2, 3}
        Dim big = From n In numbers Where n > 1 Select n
        Console.WriteLine("  count = " & big.Count())
        Dim zero As Integer = 0
        Console.WriteLine(1 \ zero)
        Return
Handler:
        Console.WriteLine("  handler: " & Err.Description)
    End Sub

    Private Sub LambdaThrows()
        On Error GoTo Handler
        Dim bad As Action = Sub() Throw New InvalidOperationException("raised in the lambda")
        bad()
        Return
Handler:
        Console.WriteLine("  handler: " & Err.Description)
    End Sub

    Sub Main()
        Console.WriteLine("On Error + Using:")
        WithUsing()
        Console.WriteLine("On Error + lambda:")
        WithLambda()
        Console.WriteLine("On Error + query:")
        WithQuery()
        Console.WriteLine("Lambda throws, caller has the handler:")
        LambdaThrows()
    End Sub
End Module
On Error + Using:
  inside Using
  Tracker disposed
  handler: Attempted to divide by zero.
On Error + lambda:
  twice(4) = 8
  handler: Attempted to divide by zero.
On Error + query:
  count = 2
  handler: Attempted to divide by zero.
Lambda throws, caller has the handler:
  handler: raised in the lambda

Source: BC36595 and BC36013

The compiler did not emit BC36595 or BC36013 in any of these situations, so treat them as legacy diagnostics that the Learn index still lists. The rules that remain are BC30544 (with Try), BC36668 (inside a lambda) and BC36956 (in Async and iterator methods).

Code Message Cause

BC30544

Method cannot contain both a 'Try' statement and an 'On Error' or 'Resume' statement.

mixing the two models in one method

BC30132

Label 'Missing' is not defined.

On Error GoTo to an undefined label

BC36956

'On Error' and 'Resume' cannot appear inside async or iterator methods.

On Error in an Async/Iterator method

BC36668

'On Error' and 'Resume' cannot appear inside a lambda expression.

On Error in a lambda body

Why it exists, and why not to use it

  • Compatibility. VB6 and VBA code uses it everywhere, and the upgrade tools and hand ports keep it. See The VB Family: VB6, VBA and VBScript.

  • It trades scoping for jumps. The handler covers the rest of the procedure, not a block, so you cannot say "this call may fail". There is no Finally: the clean-up goes behind a label and every path has to reach it.

  • Resume Next hides failures. The statement after the failure runs with whatever state the failure left. The program continues with wrong data.

  • It cannot tell exceptions apart. Err.Number collapses many .NET exceptions into a few numbers (see the table above).

  • It does not compose with the rest of .NET. It is not allowed in Async methods and lambdas, and Using already gives structured clean-up.

  • The C# language has nothing like it.

Migrating to Try

The program below puts each legacy pattern next to its Try-style replacement and prints the results of both, which are identical. The patterns:

Legacy Replacement

On Error GoTo Handler with a clean-up label, Resume CleanUp

Try ... Catch ... Finally, or Using

On Error Resume Next to skip bad items, If Err.Number = 0

TryParse/TryGetValue, or Try around each item

Resume after repairing the cause

a loop with Try and a retry counter, or a plain check before the call

On Error GoTo 0

leaving the Try block; there is no handler to disable

Err.Raise(5, ...), Error n

Throw New ArgumentException(...) or another specific type

If Err.Number = 11

Catch ex As DivideByZeroException (or Catch ... When)

Err.Description

ex.Message; Err.Source is ex.Source

Err.Clear()

nothing: an exception ends when its Catch block ends

Erl

the stack trace (ex.StackTrace, ex.ToString()), or the logger

Option Strict On
Imports System.IO

Module Legacy
    ' Pattern 1: error handler with a cleanup label
    Function ReadFirstLineOld(path As String) As String
        Dim reader As StreamReader = Nothing
        Dim result As String = "(none)"
        On Error GoTo Handler
        reader = New StreamReader(path)
        result = reader.ReadLine()
CleanUp:
        If reader IsNot Nothing Then reader.Dispose()
        Return result
Handler:
        result = "error " & Err.Number & ": " & Err.Description
        Resume CleanUp
    End Function

    ' Pattern 2: On Error Resume Next to skip bad items
    Function SumCopiesOld(fields() As String) As Integer
        Dim total As Integer = 0
        Dim i As Integer
        On Error Resume Next
        For i = 0 To fields.Length - 1
            Dim n As Integer = 0
            n = CInt(fields(i))
            If Err.Number = 0 Then total += n
            Err.Clear()
        Next
        Return total
    End Function

    ' Pattern 3: Resume to retry after the handler repaired the cause
    Function ParseWithDefaultOld(text As String) As Integer
        Dim value As Integer
        On Error GoTo Handler
        value = Integer.Parse(text)
        Return value
Handler:
        text = "0"
        Resume
    End Function

    ' Pattern 4: Err.Raise and testing Err.Number
    Sub RenewOld(extraDays As Integer)
        If extraDays > 14 Then Err.Raise(5, "Renew", "A loan can be renewed by at most 14 days.")
        Console.WriteLine("  renewed by " & extraDays)
    End Sub

    Function TryRenewOld(extraDays As Integer) As String
        On Error Resume Next
        RenewOld(extraDays)
        If Err.Number = 5 Then Return "refused: " & Err.Description
        Return "ok"
    End Function
End Module

Module Modern
    Function ReadFirstLineNew(path As String) As String
        Try
            Using reader As New StreamReader(path)
                Return reader.ReadLine()
            End Using
        Catch ex As FileNotFoundException
            Return "error 53: " & ex.Message
        End Try
    End Function

    Function SumCopiesNew(fields() As String) As Integer
        Dim total As Integer = 0
        For Each field As String In fields
            Dim n As Integer
            If Integer.TryParse(field, n) Then total += n
        Next
        Return total
    End Function

    Function ParseWithDefaultNew(text As String) As Integer
        Dim value As Integer
        If Not Integer.TryParse(text, value) Then value = 0
        Return value
    End Function

    Sub RenewNew(extraDays As Integer)
        If extraDays > 14 Then Throw New InvalidOperationException("A loan can be renewed by at most 14 days.")
        Console.WriteLine("  renewed by " & extraDays)
    End Sub

    Function TryRenewNew(extraDays As Integer) As String
        Try
            RenewNew(extraDays)
            Return "ok"
        Catch ex As InvalidOperationException
            Return "refused: " & ex.Message
        End Try
    End Function
End Module

Module Program
    Sub Main()
        Dim missing As String = Path.Combine(Path.GetTempPath(), "shelf-no-such-file.txt")
        Console.WriteLine("1. cleanup label")
        Console.WriteLine("  old: " & ReadFirstLineOld(missing).Replace(missing, "<path>"))
        Console.WriteLine("  new: " & ReadFirstLineNew(missing).Replace(missing, "<path>"))

        Dim fields() As String = {"3", "two", "4", "", "5"}
        Console.WriteLine("2. skip bad items")
        Console.WriteLine("  old: " & SumCopiesOld(fields))
        Console.WriteLine("  new: " & SumCopiesNew(fields))

        Console.WriteLine("3. repair and retry")
        Console.WriteLine("  old: " & ParseWithDefaultOld("abc"))
        Console.WriteLine("  new: " & ParseWithDefaultNew("abc"))

        Console.WriteLine("4. raising errors")
        Console.WriteLine("  old: " & TryRenewOld(7))
        Console.WriteLine("  old: " & TryRenewOld(30))
        Console.WriteLine("  new: " & TryRenewNew(7))
        Console.WriteLine("  new: " & TryRenewNew(30))
    End Sub
End Module
1. cleanup label
  old: error 53: Could not find file '<path>'.
  new: error 53: Could not find file '<path>'.
2. skip bad items
  old: 12
  new: 12
3. repair and retry
  old: 0
  new: 0
4. raising errors
  renewed by 7
  old: ok
  old: refused: A loan can be renewed by at most 14 days.
  renewed by 7
  new: ok
  new: refused: A loan can be renewed by at most 14 days.

The migration is mechanical for small procedures. For large ones, convert a procedure at a time, keep the tests (see Testing) green, and decide per Resume Next whether the ignored failure was intended. Many are not, and the right replacement for them is an explicit check or letting the exception through.

Where introductory material is out of date

  • Books and courses that teach On Error GoTo as the way to handle errors in Visual Basic describe VB6; for VB.NET the structured model is the recommended one, and the language documentation says so.

  • The Err object does not give you the type of the error; the numbers are the VB6 ones, and many .NET exceptions share a number.

  • The rules that name lambdas, queries and Using as incompatible with On Error are out of date for the 17.13 compiler. The rules about Try, lambda bodies, Async and iterators are current.

  • On Error is not available as a general error handler in C#; code shared with C# callers sees ordinary exceptions.

  • Windows Script Host and VBScript have their own On Error Resume Next, with no GoTo form; that is a different language.

Version notes and pitfalls

Topic Note

VB6 and VBA

the original home of On Error, Resume, Err, Erl; VBA still uses it

Visual Basic .NET

kept for compatibility; structured handling was added next to it; Err is backed by the .NET exception

.NET 10, VB 17.13

compiles and runs on .NET; not allowed in lambdas, Async and iterator methods; mixing with Try is BC30544

Platforms

the same on every platform; only Err.LastDllError is meaningful with Windows calls

Pitfalls:

  • Forgetting Exit Sub/Return before the handler label, so the handler runs on the normal path.

  • Resume with a handler that does not fix the cause: an infinite loop.

  • On Error Resume Next at the top of a long procedure: every later failure is invisible. Limit its scope to a small helper.

  • Reading Err.Number after another call that can fail, or after code that raised and handled a second error: the object only describes the latest error. Copy Err.Number and Err.Description into locals first.

  • Assuming Resume Next in the caller resumes after the line in the callee: it resumes after the call.

  • Mixing the model with Try in one method (BC30544); a method that needs both has to be split.