Structured Exception Handling

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 a warning is the point, and the page says so) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The project’s root namespace is ShelfDocs, which is why CLR messages and stack frames show names such as ShelfDocs.Program.Main. Compiler diagnostics are shown with the file name Program.vb, and absolute build paths in stack traces are shortened. The error examples are separate failing programs, because a declaration-level or syntax error hides the errors inside method bodies. The project-level imports that the SDK adds (System, System.Collections.Generic, System.Linq, System.Threading.Tasks and others) are why Console, List(Of T) and Task need no Imports line; System.IO is imported where files or readers appear.

Visual Basic reports run-time failures with exceptions: objects derived from System.Exception that are thrown by Throw or by the runtime and caught by a Try block further up the call stack. The model is the one the whole .NET platform shares, so a VB program catches exceptions thrown by C# libraries and the other way round. The older VB6 model (On Error GoTo, Resume Next, the Err object) still compiles and is covered on Unstructured Error Handling: On Error; new code should not use it, and it cannot be combined with Try in the same method. The C# side of the same material is on C# Exceptions and Error Handling.

Construct Purpose Related page

Try ... Catch ... Finally ... End Try

run code, handle selected exceptions, always run cleanup

this page

Catch ex As T When condition

catch only when a condition on the exception holds (a CLR exception filter)

this page

Throw, Throw expression

raise an exception, or rethrow the one being handled

this page

Exit Try

leave the Try or Catch part early (the Finally still runs)

this page

Using ... End Using

dispose a resource even when an exception is thrown

Object Lifetime and Disposal

Err, On Error, Resume

the VB6 model, kept for compatibility

On Error

Async/Await

exceptions travel in the returned Task

Async and Await

How a Try statement executes

A Try statement has a Try block, any number of Catch blocks and an optional Finally block; at least one Catch or the Finally is required (BC30030 otherwise). When code in the Try block throws, the runtime searches the Catch blocks of that statement from top to bottom, then of the enclosing Try statements in the same method, then of the callers, for the first block whose type matches and whose When condition is True. Only after that search has found a handler does the runtime unwind the stack, running the Finally blocks of the frames it leaves, and then enter the Catch block. If the search finds nothing, the exception is unhandled and the process ends (see Global handlers below).

flowchart TD A(["Code in a Try block throws"]) --> B{"Search: a Catch whose type matches and whose When is True?"} B -->|"yes, in this Try or a caller"| C["Unwind the stack and run the Finally blocks of the frames being left"] C --> D["Run the matching Catch block"] D --> E["Run the Finally block of the Try that caught it"] E --> F(["Continue after End Try"]) B -->|"no handler anywhere"| G["Unhandled: UnhandledException event, then the process ends"]

Try, Catch and Finally

The first example parses text with Integer.Parse and handles three cases with three Catch blocks. Each block names an exception type, so only that type (and types derived from it) enter it. The Finally block runs after every iteration, whether the Try block completed or a Catch block ran.

Option Strict On

Module Program
    Sub Main()
        For Each text As String In {"3", "three", "", "99999999999"}
            Try
                Dim copies As Integer = Integer.Parse(text)
                Console.WriteLine("copies = " & copies)
            Catch ex As OverflowException
                Console.WriteLine("too big: " & ex.GetType().Name)
            Catch ex As FormatException
                Console.WriteLine("not a number: " & ex.Message)
            Catch ex As Exception
                Console.WriteLine("anything else: " & ex.GetType().Name)
            Finally
                Console.WriteLine("  finished '" & text & "'")
            End Try
        Next
    End Sub
End Module
copies = 3
  finished '3'
not a number: The input string 'three' was not in a correct format.
  finished 'three'
not a number: The input string '' was not in a correct format.
  finished ''
too big: OverflowException
  finished '99999999999'

The rules in one place:

Rule Detail

Matching

a Catch block handles an exception when the exception is an instance of the named type, including derived types; Catch ex As Exception handles everything that derives from Exception

Order

the first matching block wins; blocks are not searched for the best match, so put derived types before base types

Variable scope

the variable declared in Catch ex As T exists only inside that block, so sibling blocks can reuse the name ex; reusing it for another local in the same method is BC30616

Finally

runs when the Try block ends normally, when a Catch block runs, when Exit Try, Return or Exit Sub leaves the statement, and when an exception goes unhandled to an outer handler

Locals

a variable declared inside the Try block is not visible in Catch or Finally (BC30451); declare it before the Try

Result

a Try statement is a statement, not an expression; it has no value

Catch order

Because the first matching block wins, a base type placed before a derived type makes the derived block unreachable. The compiler notices it, but as a warning, not an error: BC42029 is a warning, so it becomes an error only when the project treats warnings as errors (as the other examples on this page do).

Option Strict On

Module Program
    Sub Main()
        Try
            Console.WriteLine("a")
        Catch ex As Exception
            Console.WriteLine("general")
        Catch ex As ArgumentException
            Console.WriteLine("specific")
        End Try
    End Sub
End Module

Built without TreatWarningsAsErrors; the compiler printed:

Program.vb(9,9): warning BC42029: 'Catch' block never reached, because 'ArgumentException' inherits from 'Exception'.

Put Catch ex As ArgumentException first and Catch ex As Exception last. Catching Exception is legitimate at the outermost layer of a program (a request handler, a worker loop, Sub Main) where the aim is to log and continue or stop cleanly; deep in a library it hides bugs, which is what the code-analysis rule CA1031 warns about.

Catch forms

A bare Catch with no type handles everything, and each Catch block has its own scope for the exception variable. When may use TypeOf ... Is to treat several types alike without repeating the body.

Option Strict On

Module Program
    Function Quiet() As Integer
        Try
            Return 1
        Catch
            Return -1
        End Try
    End Function

    Sub Main()
        ' Catch without a type catches Exception
        Try
            Throw New FormatException("bare Catch")
        Catch
            Console.WriteLine("caught without naming a type")
        End Try

        ' Each Catch block has its own scope, so the same name can be reused
        For Each code As Integer In {1, 2}
            Try
                If code = 1 Then Throw New ArgumentException("one")
                Throw New InvalidOperationException("two")
            Catch ex As ArgumentException
                Console.WriteLine("A: " & ex.Message)
            Catch ex As InvalidOperationException
                Console.WriteLine("B: " & ex.Message)
            End Try
        Next

        ' TypeOf in a filter instead of a separate Catch
        Try
            Throw New TimeoutException("slow")
        Catch ex As Exception When TypeOf ex Is TimeoutException OrElse TypeOf ex Is OperationCanceledException
            Console.WriteLine("timeout or cancellation: " & ex.GetType().Name)
        End Try

        Console.WriteLine(Quiet())
    End Sub
End Module
caught without naming a type
A: one
B: two
timeout or cancellation: TimeoutException
1

In Catch ex As Exception the As clause is what declares ex. Writing Catch ex without a type does not declare a variable; it refers to an existing one, which is why it fails when there is none (BC30451):

Option Strict On

Module Program
    Sub Main()
        Try
            Throw New KeyNotFoundException("short form")
        Catch ex
            Console.WriteLine(ex.Message)
        End Try
    End Sub
End Module
Program.vb(7,15): error BC30451: 'ex' is not declared. It may be inaccessible due to its protection level.
Program.vb(8,31): error BC30451: 'ex' is not declared. It may be inaccessible due to its protection level.

Exception filters: Catch …​ When

Catch ex As T When condition adds a Boolean expression to the match. If the expression is False, the runtime behaves as if the block were not there and keeps searching. Visual Basic compiles When to a real CLR exception filter, not to an If inside the block followed by a rethrow, and the difference is observable:

  • The filter runs during the search phase, before the Finally blocks of inner frames run. In the first case below, the filter prints before inner Finally.

  • Because the stack has not been unwound when the filter runs, the original stack trace and the debugger’s view of the throwing frame are intact.

  • An exception thrown inside a filter is swallowed and counts as False (case 3).

  • A filter that logs and returns False observes every exception without ever catching it (case 4). Compare this with Catch ex As Exception followed by Throw, which changes the trace and unwinds first.

Option Strict On

Module Program
    Private Function Check(label As String, result As Boolean) As Boolean
        Console.WriteLine("  filter " & label & " -> " & result)
        Return result
    End Function

    Private Sub Inner()
        Try
            Throw New InvalidOperationException("loan table locked")
        Finally
            Console.WriteLine("  inner Finally")
        End Try
    End Sub

    Private Function Faulty() As Boolean
        Throw New NotSupportedException("filter bug")
    End Function

    Sub Main()
        Console.WriteLine("1. filters run before inner Finally blocks:")
        Try
            Inner()
        Catch ex As InvalidOperationException When Check("outer", True)
            Console.WriteLine("  caught " & ex.Message)
        End Try

        Console.WriteLine("2. a False filter lets the next Catch look:")
        Try
            Throw New ArgumentException("code 7")
        Catch ex As ArgumentException When ex.Message.EndsWith("9")
            Console.WriteLine("  nine")
        Catch ex As ArgumentException When ex.Message.EndsWith("7")
            Console.WriteLine("  seven")
        Catch ex As ArgumentException
            Console.WriteLine("  other")
        End Try

        Console.WriteLine("3. an exception inside a filter is swallowed (filter = False):")
        Try
            Try
                Throw New InvalidOperationException("original")
            Catch ex As InvalidOperationException When Faulty()
                Console.WriteLine("  never printed")
            End Try
        Catch ex As Exception
            Console.WriteLine("  outer caught " & ex.GetType().Name & ": " & ex.Message)
        End Try

        Console.WriteLine("4. logging filter that never catches:")
        Try
            Try
                Throw New TimeoutException("lookup timed out")
            Catch ex As Exception When Check("log " & ex.Message, False)
                Console.WriteLine("  never printed")
            End Try
        Catch ex As TimeoutException
            Console.WriteLine("  handled by the outer Catch")
        End Try
    End Sub
End Module
1. filters run before inner Finally blocks:
  filter outer -> True
  inner Finally
  caught loan table locked
2. a False filter lets the next Catch look:
  seven
3. an exception inside a filter is swallowed (filter = False):
  outer caught InvalidOperationException: original
4. logging filter that never catches:
  filter log lookup timed out -> False
  handled by the outer Catch

Keep filters short, free of side effects other than logging, and cheap: they run for exceptions the block will not handle too. Typical uses are matching on a property (ex.HResult, ex.ParamName, ex.StatusCode), on the type of the inner exception, or on a retry counter held in a local variable.

Finally, Exit Try and Using

Finally is for cleanup that must happen on every path. Exit Try leaves the Try or Catch part immediately, skipping the rest of that block, but it does not skip the Finally. A Return inside Try evaluates its value first, then runs the Finally, then returns. A Using statement is a Try ... Finally that calls Dispose: the resources are disposed (in reverse order of declaration) before the Catch block of an enclosing Try runs, because the unwinding happens before the handler starts.

Option Strict On

Public Class Connection
    Implements IDisposable

    Private ReadOnly _name As String

    Public Sub New(name As String)
        _name = name
        Console.WriteLine("  open " & _name)
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Console.WriteLine("  close " & _name)
    End Sub
End Class

Module Program
    Private Function Work() As String
        Try
            Return "value from Try"
        Finally
            Console.WriteLine("  Finally runs after Return was evaluated")
        End Try
    End Function

    Sub Main()
        Console.WriteLine("Using inside Try:")
        Try
            Using first As New Connection("catalog"), second As New Connection("loans")
                Console.WriteLine("  working")
                Throw New InvalidOperationException("lost connection")
            End Using
        Catch ex As InvalidOperationException
            Console.WriteLine("  Catch: " & ex.Message)
        Finally
            Console.WriteLine("  Finally")
        End Try

        Console.WriteLine("Exit Try still runs Finally:")
        Try
            Console.WriteLine("  before Exit Try")
            Exit Try
            Console.WriteLine("  never printed")
        Finally
            Console.WriteLine("  Finally")
        End Try

        Console.WriteLine(Work())

        Console.WriteLine("An exception thrown in Finally replaces the original:")
        Try
            Try
                Throw New InvalidOperationException("original")
            Finally
                Throw New ApplicationException("from Finally")
            End Try
        Catch ex As Exception
            Console.WriteLine("  " & ex.GetType().Name & ": " & ex.Message & " (inner is Nothing: " & (ex.InnerException Is Nothing) & ")")
        End Try
    End Sub
End Module
Using inside Try:
  open catalog
  open loans
  working
  close loans
  close catalog
  Catch: lost connection
  Finally
Exit Try still runs Finally:
  before Exit Try
  Finally
  Finally runs after Return was evaluated
value from Try
An exception thrown in Finally replaces the original:
  ApplicationException: from Finally (inner is Nothing: True)

Source: Using Statement

Two rules matter in Finally:

  • An exception thrown from Finally replaces the one in flight. The original is lost unless you wrap it as the InnerException of the new one. So Finally code should be small and should not throw; Dispose implementations must not throw.

  • Control cannot leave a Finally block: Return, Exit For, Exit Do, GoTo and Exit Sub out of it are errors (BC30101). C# has the same rule (CS0157).

Option Strict On

Module Program
    Function Finish() As Integer
        Try
            Return 1
        Finally
            Return 2
        End Try
    End Function
    Sub Loop2()
        For i As Integer = 1 To 3
            Try
                Console.WriteLine(i)
            Finally
                Exit For
            End Try
        Next
    End Sub
    Sub Main()
    End Sub
End Module
Program.vb(8,13): error BC30101: Branching out of a 'Finally' is not valid.
Program.vb(16,17): error BC30101: Branching out of a 'Finally' is not valid.

Cleanup of memory is not the job of Finally: the garbage collector reclaims managed memory. Use Finally/Using for files, sockets, locks, database connections, temporary state and anything with a Dispose. See Object Lifetime and Disposal.

Throw and rethrow

Throw expression raises an exception object (it must derive from System.Exception). A bare Throw inside a Catch block rethrows the exception being handled and preserves its original stack trace. Throw ex also compiles, but it throws the same object as if it were new at that point: the trace restarts at the Throw ex statement and the frames below it are lost. The analyzer CA2200 warns about this, and it is a warning by default, so the example below was built without TreatWarningsAsErrors. (With warnings as errors the SDK reports error CA2200 for the same line.) Throwing a new exception that carries the caught one as its InnerException, the third case, keeps both traces.

Option Strict On

Public Class CatalogException
    Inherits Exception

    Public Sub New(message As String, innerException As Exception)
        MyBase.New(message, innerException)
    End Sub
End Class

Module Program
    Private Sub Level3()
        Throw New InvalidOperationException("level 3 failed")
    End Sub

    Private Sub Level2(mode As Integer)
        Try
            Level3()
        Catch ex As InvalidOperationException
            Select Case mode
                Case 1
                    Throw                     ' rethrow, keeps the original stack trace
                Case 2
                    Throw ex                  ' throws the same object, restarts the stack trace here
                Case Else
                    Throw New CatalogException("catalog lookup failed", ex)
            End Select
        End Try
    End Sub

    Private Sub Level1(mode As Integer)
        Level2(mode)
    End Sub

    Private Function Frames(ex As Exception) As String
        Dim names As New List(Of String)
        For Each line As String In ex.StackTrace.Split(New String() {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
            Dim text As String = line.Trim().Substring(3)
            names.Add(text.Substring(0, text.IndexOf("("c)))
        Next
        Return String.Join(" <- ", names)
    End Function

    Sub Main()
        For mode As Integer = 1 To 3
            Try
                Level1(mode)
            Catch ex As Exception
                Console.WriteLine("mode " & mode & ": " & ex.GetType().Name & " | " & Frames(ex))
                If ex.InnerException IsNot Nothing Then
                    Console.WriteLine("        inner: " & ex.InnerException.GetType().Name & " | " & Frames(ex.InnerException))
                End If
            End Try
        Next
    End Sub
End Module

Built with one warning (warning CA2200: Re-throwing caught exception changes stack information). Output:

mode 1: InvalidOperationException | ShelfDocs.Program.Level3 <- ShelfDocs.Program.Level2 <- ShelfDocs.Program.Level1 <- ShelfDocs.Program.Main
mode 2: InvalidOperationException | ShelfDocs.Program.Level2 <- ShelfDocs.Program.Level1 <- ShelfDocs.Program.Main
mode 3: CatalogException | ShelfDocs.Program.Level2 <- ShelfDocs.Program.Level1 <- ShelfDocs.Program.Main
        inner: InvalidOperationException | ShelfDocs.Program.Level3 <- ShelfDocs.Program.Level2

In mode 1 the trace still starts in Level3, where the failure happened. In mode 2, after Throw ex, Level3 is gone from the trace. In mode 3 the new CatalogException starts in Level2 and the original, with its full trace, is its InnerException.

To keep a trace while moving an exception between threads or to a later point, capture it with ExceptionDispatchInfo (shown in the properties example below); rethrowing it appends to the original trace instead of replacing it.

Throw is a statement, not an expression

C# can write value = text ?? throw new ArgumentNullException(...), and C# 7.0 allows throw in a conditional or lambda expression body. Visual Basic has no throw expression: Throw is a statement and cannot appear where a value is expected. Use a guard clause (ArgumentNullException.ThrowIfNull, below), an If statement, or a small helper Function that throws and is used as the second argument of If(a, b).

Option Strict On

Module Program
    Private Function Fail() As Integer
        Throw New InvalidOperationException("fail")
    End Function

    Sub Main()
        ' C# writes: value = text ?? throw new ArgumentNullException(...)   VB has no throw expression.
        Dim text As String = Nothing
        Try
            Dim value As String = If(text, Throw())
        Catch ex As Exception
        End Try
    End Sub
End Module
Program.vb(12,44): error BC30201: Expression expected.

Related compile-time rules for Throw and Exit Try, each shown in a separate failing program because the first group are statement-level errors that the compiler reports together:

Option Strict On

Module Program
    Sub Main()
        Throw "boom"
    End Sub
    Sub Other()
        Try
            Console.WriteLine("a")
        Catch ex As String
        End Try
    End Sub
End Module
Program.vb(5,9): error BC30665: 'Throw' operand must derive from 'System.Exception'.
Program.vb(10,21): error BC30392: 'Catch' cannot catch type 'String' because it is not 'System.Exception' or a class that inherits from 'System.Exception'.
Option Strict On

Module Program
    Sub Main()
        Exit Try
    End Sub
    Sub Other()
        Throw
    End Sub
End Module
Program.vb(5,9): error BC30393: 'Exit Try' can only appear inside a 'Try' statement.
Program.vb(8,9): error BC30666: 'Throw' statement cannot omit operand outside a 'Catch' statement or inside a 'Finally' statement.

Exception properties

The members worth knowing, shown in one program: Message, Source (the assembly name by default), TargetSite, HResult, the Data dictionary for extra context, InnerException and GetBaseException, AggregateException.Flatten, and ExceptionDispatchInfo.

Option Strict On
Imports System.Runtime.ExceptionServices

Module Program
    Private Function Boom(code As Integer) As Exception
        Try
            Throw New InvalidOperationException("boom " & code)
        Catch ex As InvalidOperationException
            Return ex
        End Try
    End Function

    Sub Main()
        Dim never As New InvalidOperationException("never thrown")
        Console.WriteLine("StackTrace of an exception that was never thrown is Nothing: " & (never.StackTrace Is Nothing))

        Dim ex As Exception = Boom(1)
        ex.Data("isbn") = "9780306406157"
        ex.Data("attempt") = 2
        ex.HelpLink = "https://example.org/help/boom"
        Console.WriteLine("Message   : " & ex.Message)
        Console.WriteLine("Source    : " & ex.Source)
        Console.WriteLine("TargetSite: " & ex.TargetSite.Name)
        Console.WriteLine("HResult   : 0x" & ex.HResult.ToString("X8"))
        For Each entry As System.Collections.DictionaryEntry In ex.Data
            Console.WriteLine("Data      : " & entry.Key.ToString() & " = " & entry.Value.ToString())
        Next
        Console.WriteLine("GetBaseException: " & ex.GetBaseException().Message)

        Dim wrapped As New InvalidOperationException("outer", New FormatException("middle", New ArgumentException("inner")))
        Console.WriteLine("Base exception of a chain: " & wrapped.GetBaseException().GetType().Name)
        Dim current As Exception = wrapped
        Dim depth As Integer = 0
        Do While current IsNot Nothing
            Console.WriteLine(New String(" "c, depth * 2) & current.GetType().Name & ": " & current.Message)
            current = current.InnerException
            depth += 1
        Loop

        Dim agg As New AggregateException("several", New Exception() {
            New TimeoutException("a"),
            New AggregateException(New Exception() {New FormatException("b"), New KeyNotFoundException("c")})})
        Console.WriteLine("Flatten: " & String.Join(", ", agg.Flatten().InnerExceptions.Select(Function(e) e.GetType().Name)))

        ' Capture an exception here and rethrow it later, keeping its original stack trace
        Dim captured As ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(Boom(2))
        Try
            captured.Throw()
        Catch e2 As InvalidOperationException
            Console.WriteLine("Rethrown with ExceptionDispatchInfo: " & e2.Message & ", trace still mentions Boom: " & e2.StackTrace.Contains("Boom"))
        End Try
    End Sub
End Module
StackTrace of an exception that was never thrown is Nothing: True
Message   : boom 1
Source    : ex09
TargetSite: Boom
HResult   : 0x80131509
Data      : isbn = 9780306406157
Data      : attempt = 2
GetBaseException: boom 1
Base exception of a chain: ArgumentException
InvalidOperationException: outer
  FormatException: middle
    ArgumentException: inner
Flatten: TimeoutException, FormatException, KeyNotFoundException
Rethrown with ExceptionDispatchInfo: boom 2, trace still mentions Boom: True
Member Notes

Message

human-readable description; for the user or the log, not for program logic

StackTrace

Nothing until the exception is thrown; a text rendering of the frames between the throw and the catch

InnerException

the exception that caused this one; walk it in a loop, or use GetBaseException() for the innermost

Data

an IDictionary for values that explain the failure (an ISBN, an attempt count)

HResult

a number; 0x80131509 is the default for InvalidOperationException, 0x80070002 for FileNotFoundException

Source, TargetSite

assembly name and the method that threw

HelpLink

a URL or file for documentation; rarely used

ToString()

type, message, inner exceptions and stack trace in one string: the right thing to log

The exception hierarchy

Every exception derives from System.Exception. Almost all of the exceptions in the base libraries derive from SystemException or directly from Exception. This program prints the base-type chain of the ones used most often.

Option Strict On
Imports System.IO

Module Program
    Private Sub Show(ex As Exception)
        Dim chain As New List(Of String)
        Dim t As Type = ex.GetType()
        Do While t IsNot Nothing
            chain.Add(t.Name)
            t = t.BaseType
        Loop
        Console.WriteLine(String.Join(" > ", chain))
    End Sub

    Sub Main()
        Show(New FileNotFoundException())
        Show(New ArgumentNullException())
        Show(New ArgumentOutOfRangeException())
        Show(New InvalidCastException())
        Show(New NullReferenceException())
        Show(New DivideByZeroException())
        Show(New OverflowException())
        Show(New KeyNotFoundException())
        Show(New ObjectDisposedException("x"))
        Show(New FormatException())
        Show(New OperationCanceledException())
        Show(New AggregateException())
        Show(New TimeoutException())
        Show(New InvalidOperationException())
        Show(New NotImplementedException())
        Show(New StackOverflowException())
        Show(New OutOfMemoryException())
    End Sub
End Module
FileNotFoundException > IOException > SystemException > Exception > Object
ArgumentNullException > ArgumentException > SystemException > Exception > Object
ArgumentOutOfRangeException > ArgumentException > SystemException > Exception > Object
InvalidCastException > SystemException > Exception > Object
NullReferenceException > SystemException > Exception > Object
DivideByZeroException > ArithmeticException > SystemException > Exception > Object
OverflowException > ArithmeticException > SystemException > Exception > Object
KeyNotFoundException > SystemException > Exception > Object
ObjectDisposedException > InvalidOperationException > SystemException > Exception > Object
FormatException > SystemException > Exception > Object
OperationCanceledException > SystemException > Exception > Object
AggregateException > Exception > Object
TimeoutException > SystemException > Exception > Object
InvalidOperationException > SystemException > Exception > Object
NotImplementedException > SystemException > Exception > Object
StackOverflowException > SystemException > Exception > Object
OutOfMemoryException > SystemException > Exception > Object

Three consequences:

  • FileNotFoundException derives from IOException, and ObjectDisposedException from InvalidOperationException, so order matters when catching both a family and a member.

  • ArgumentNullException and ArgumentOutOfRangeException derive from ArgumentException; DivideByZeroException and OverflowException from ArithmeticException.

  • OperationCanceledException is not an IOException or an ArgumentException; it is the normal way a cancelled operation ends and is usually caught on its own (see Async and Await).

The exceptions you meet most often

The program below triggers the common run-time exceptions on purpose, with the messages that .NET 10 produced. Visual Basic’s conversion functions raise their own flavour of errors, which is why CInt("12x") throws InvalidCastException, not the FormatException that Integer.Parse throws, and why CInt on a boxed String fails the same way.

Option Strict On
Imports System.IO

Module Program
    Private Sub Probe(label As String, action As Action)
        Try
            action()
            Console.WriteLine(label & ": no exception")
        Catch ex As Exception
            Console.WriteLine(label & ": " & ex.GetType().Name & " - " & ex.Message.Replace(Environment.CurrentDirectory, "."))
        End Try
    End Sub

    Sub Main()
        Dim title As String = Nothing
        Dim anything As Object = "text"
        Dim titles As New List(Of String) From {"Dune"}
        Dim numbers() As Integer = {1, 2, 3}
        Dim byIsbn As New Dictionary(Of String, String)
        Dim big As Integer = Integer.MaxValue
        Dim index As Integer = 5

        Probe("NullReference", Sub() Console.WriteLine(title.Length))
        Probe("InvalidCast", Sub() Console.WriteLine(CInt(anything)))
        Probe("IndexOutOfRange", Sub() Console.WriteLine(numbers(index)))
        Probe("ArgumentOutOfRange", Sub() Console.WriteLine(titles(index)))
        Probe("KeyNotFound", Sub() Console.WriteLine(byIsbn("9780306406157")))
        Probe("InvalidOperation (collection changed)", Sub()
                                                           For Each t As String In titles
                                                               titles.Add(t)
                                                           Next
                                                       End Sub)
        Probe("Format", Sub() Console.WriteLine(CInt("12x")))
        Probe("Overflow (arithmetic)", Sub() Console.WriteLine(big + 1))
        Probe("Overflow (conversion)", Sub() Console.WriteLine(CByte(300 + index)))
        Probe("ObjectDisposed", Sub()
                                    Dim reader As New StringReader("x")
                                    reader.Dispose()
                                    reader.ReadToEnd()
                                End Sub)
        Probe("FileNotFound", Sub() File.ReadAllText("no-such-file.txt"))
        Probe("DirectoryNotFound", Sub() File.ReadAllText("no-such-dir/file.txt"))
        Probe("ArrayTypeMismatch", Sub()
                                       Dim objects() As Object = New String(2) {}
                                       objects(0) = 1
                                   End Sub)
        Probe("NotSupported", Sub() DirectCast(titles.AsReadOnly(), IList(Of String)).Add("x"))
        Probe("Integer \ 0", Sub() Console.WriteLine(big \ (index - 5)))
    End Sub
End Module
NullReference: NullReferenceException - Object reference not set to an instance of an object.
InvalidCast: InvalidCastException - Conversion from string "text" to type 'Integer' is not valid.
IndexOutOfRange: IndexOutOfRangeException - Index was outside the bounds of the array.
ArgumentOutOfRange: ArgumentOutOfRangeException - Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
KeyNotFound: KeyNotFoundException - The given key '9780306406157' was not present in the dictionary.
InvalidOperation (collection changed): InvalidOperationException - Collection was modified; enumeration operation may not execute.
Format: InvalidCastException - Conversion from string "12x" to type 'Integer' is not valid.
Overflow (arithmetic): OverflowException - Arithmetic operation resulted in an overflow.
Overflow (conversion): OverflowException - Arithmetic operation resulted in an overflow.
ObjectDisposed: ObjectDisposedException - Cannot read from a closed TextReader.
FileNotFound: FileNotFoundException - Could not find file './no-such-file.txt'.
DirectoryNotFound: DirectoryNotFoundException - Could not find a part of the path './no-such-dir/file.txt'.
ArrayTypeMismatch: ArrayTypeMismatchException - Attempted to access an element as a type incompatible with the array.
NotSupported: NotSupportedException - Collection is read-only.
Integer \ 0: DivideByZeroException - Attempted to divide by zero.
Exception Typical cause in VB What to do

NullReferenceException

member access on Nothing, often an uninitialised field or a Dictionary lookup that returned Nothing

test with Is Nothing or IsNot Nothing, use ?., fix the cause; do not catch it

InvalidCastException

DirectCast/CType/CInt of an incompatible value; CInt/CDbl on non-numeric text

TypeOf ... Is, TryCast, or the TryParse methods

IndexOutOfRangeException, ArgumentOutOfRangeException

array or list index beyond the end; VB array bounds are upper bounds, so Dim a(5) has six elements

check Length/Count

KeyNotFoundException

dictionary(key) for a missing key

TryGetValue or ContainsKey

InvalidOperationException

changing a collection while a For Each runs over it; Enumerable.Max of an empty sequence; wrong object state

iterate a copy or by index; check state first

FormatException, OverflowException

Integer.Parse on bad text, arithmetic past the range (overflow is checked by default in VB)

TryParse, wider type, Decimal

ObjectDisposedException

using an object after Dispose

scope with Using; do not cache disposed objects

FileNotFoundException, DirectoryNotFoundException, UnauthorizedAccessException, IOException

file system access

catch these (they can happen even after an existence check); see Files, Directories and Streams

NotSupportedException

modifying a read-only collection, unsupported operation

choose the right collection type

DivideByZeroException

\, Mod or Decimal division by zero

see the next section

Exceptions that cannot be handled

StackOverflowException, and in practice OutOfMemoryException when the process really is out of memory, are not something a Try block can recover from. A stack overflow ends the process immediately: neither the Catch nor the Finally below runs, and the runtime writes the repeated frame and aborts (exit code 134 on Linux).

Option Strict On

Module Program
    Private Function Depth(n As Integer) As Integer
        Return Depth(n + 1) + 1
    End Function

    Sub Main()
        Try
            Console.WriteLine("calling")
            Console.WriteLine(Depth(0))
        Catch ex As Exception
            Console.WriteLine("never reached: " & ex.GetType().Name)
        Finally
            Console.WriteLine("never reached: Finally")
        End Try
    End Sub
End Module
calling
Stack overflow.
Repeated 261890 times:
--------------------------------
   at ShelfDocs.Program.Depth(Int32)
--------------------------------
   at ShelfDocs.Program.Main()
(process exit code 134)

Divide by zero, and exceptions as flow control

Introductory material often says that dividing by zero throws DivideByZeroException. In Visual Basic that is true only for some operators. / converts both operands to Double and divides, so 10 / 0 is Infinity and 0 / 0 is NaN, with no exception at all. Integer division \, Mod, and Decimal division throw. Integer.MinValue \ -1 throws OverflowException.

Option Strict On

Module Program
    Private Sub Try1(label As String, action As Func(Of Object))
        Try
            Console.WriteLine(label & " = " & Convert.ToString(action()))
        Catch ex As Exception
            Console.WriteLine(label & " throws " & ex.GetType().Name)
        End Try
    End Sub

    Private Function Average(values As IReadOnlyList(Of Integer)) As Double
        If values.Count = 0 Then
            Return 0.0
        End If
        Return CDbl(values.Sum()) / values.Count
    End Function

    Sub Main()
        Dim ten As Integer = 10
        Dim zero As Integer = 0
        Dim minusOne As Integer = -1
        Dim minimum As Integer = Integer.MinValue
        Dim d As Double = 0.0
        Dim m As Decimal = 0D

        Try1("10 / 0 (Integer / Integer)", Function() ten / zero)
        Try1("10 \ 0 (integer division)", Function() ten \ zero)
        Try1("10 Mod 0", Function() ten Mod zero)
        Try1("10.0 Mod 0.0", Function() CDbl(ten) Mod d)
        Try1("10D / 0D", Function() CDec(ten) / m)
        Try1("Integer.MinValue \ -1", Function() minimum \ minusOne)
        Try1("Math.DivRem(10, 0)", Function() Math.DivRem(ten, zero).Quotient)

        Console.WriteLine("average of nothing: " & Average(New List(Of Integer)))
        Try1("Enumerable.Average of nothing", Function() New List(Of Integer)().Average())
        Try1("Enumerable.Max of nothing", Function() New List(Of Integer)().Max())
    End Sub
End Module
10 / 0 (Integer / Integer) = Infinity
10 \ 0 (integer division) throws DivideByZeroException
10 Mod 0 throws DivideByZeroException
10.0 Mod 0.0 = NaN
10D / 0D throws DivideByZeroException
Integer.MinValue \ -1 throws OverflowException
Math.DivRem(10, 0) throws DivideByZeroException
average of nothing: 0
Enumerable.Average of nothing throws InvalidOperationException
Enumerable.Max of nothing throws InvalidOperationException

Source: \ Operator

With constants the compiler refuses to build the expression (BC30542), so the exception only appears when the divisor is a variable:

Option Strict On

Module Program
    Sub Main()
        Dim a As Integer = 10 \ 0
        Dim b As Integer = 10 Mod 0
        Console.WriteLine(a & b)
    End Sub
End Module
Program.vb(5,28): error BC30542: Division by zero occurred while evaluating this expression.
Program.vb(6,28): error BC30542: Division by zero occurred while evaluating this expression.

The practical lessons are the same as for any exception:

  • Validate the input instead of catching the failure when the failure is expected: test If divisor = 0 Then, return a default, or return a result type. Average of an empty list is a design decision (zero, Nothing, an error), not a case for Try.

  • Do not use exceptions for ordinary control flow. The Try pattern (Integer.TryParse, Dictionary.TryGetValue, the shelf’s Isbn.TryParse) produces the same results without throwing; a thrown-and-caught exception captures a stack trace and unwinds frames, which costs far more than a failed parse. The two loops below give identical results:

Option Strict On

Module Program
    Sub Main()
        Dim inputs As New List(Of String)
        For i As Integer = 0 To 9999
            inputs.Add(If(i Mod 4 = 0, "x" & i, i.ToString()))
        Next

        ' 1. Exceptions as the validation mechanism: expensive when most inputs fail
        Dim bad1 As Integer = 0
        Dim sum1 As Long = 0
        For Each text As String In inputs
            Try
                sum1 += Integer.Parse(text)
            Catch ex As FormatException
                bad1 += 1
            End Try
        Next

        ' 2. The Try pattern: no exception object, no stack trace capture
        Dim bad2 As Integer = 0
        Dim sum2 As Long = 0
        For Each text As String In inputs
            Dim value As Integer
            If Integer.TryParse(text, value) Then
                sum2 += value
            Else
                bad2 += 1
            End If
        Next

        Console.WriteLine("Parse + Catch    : bad=" & bad1 & " sum=" & sum1)
        Console.WriteLine("TryParse         : bad=" & bad2 & " sum=" & sum2)
    End Sub
End Module
Parse + Catch    : bad=2500 sum=37500000
TryParse         : bad=2500 sum=37500000

Custom exceptions and guard clauses

Derive custom exceptions from Exception (or from a more specific type), not from ApplicationException: the .NET design guidelines no longer recommend that base class, and it adds nothing. Name them with the Exception suffix, offer the standard constructors (no arguments, a message, a message and an inner exception), and add properties for the data a handler needs. Create a custom type only when callers will handle it differently from the existing ones; otherwise throw a standard type (ArgumentException, InvalidOperationException, NotSupportedException).

Guard clauses check arguments first and throw before any work is done. The throw helpers on ArgumentNullException, ArgumentException and ArgumentOutOfRangeException capture the parameter name from the argument expression, so the message names the parameter without a string literal. They are ordinary shared methods, so they work from Visual Basic exactly as from C#.

Option Strict On

Public Class CatalogException
    Inherits Exception

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(message As String)
        MyBase.New(message)
    End Sub

    Public Sub New(message As String, innerException As Exception)
        MyBase.New(message, innerException)
    End Sub
End Class

Public Class BookNotFoundException
    Inherits CatalogException

    Public ReadOnly Property IsbnText As String

    Public Sub New(isbnText As String)
        MyBase.New("No book with ISBN " & isbnText & " is in the catalog.")
        Me.IsbnText = isbnText
    End Sub
End Class

Public Class Catalog
    Private ReadOnly _titles As New Dictionary(Of String, String)

    Public Sub Add(isbnText As String, title As String, copies As Integer)
        ArgumentException.ThrowIfNullOrWhiteSpace(isbnText)
        ArgumentNullException.ThrowIfNull(title)
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(copies)
        _titles(isbnText) = title
    End Sub

    Public Function TitleOf(isbnText As String) As String
        Dim title As String = Nothing
        If Not _titles.TryGetValue(isbnText, title) Then
            Throw New BookNotFoundException(isbnText)
        End If
        Return title
    End Function
End Class

Module Program
    Sub Main()
        Dim catalog As New Catalog()
        Dim attempts As New List(Of Action) From {
            Sub() catalog.Add("9780306406157", Nothing, 1),
            Sub() catalog.Add("   ", "Dune", 1),
            Sub() catalog.Add("9780306406157", "Dune", 0),
            Sub() catalog.Add("9780306406157", "Dune", 2),
            Sub() Console.WriteLine(catalog.TitleOf("9780306406157")),
            Sub() Console.WriteLine(catalog.TitleOf("9781234567897"))
        }
        For Each attempt As Action In attempts
            Try
                attempt()
            Catch ex As ArgumentException
                Console.WriteLine(ex.GetType().Name & ": " & ex.Message.Replace(Environment.NewLine, " | ") & " [ParamName=" & ex.ParamName & "]")
            Catch ex As BookNotFoundException
                Console.WriteLine(ex.GetType().Name & ": " & ex.Message & " [IsbnText=" & ex.IsbnText & "]")
            End Try
        Next
    End Sub
End Module
ArgumentNullException: Value cannot be null. (Parameter 'title') [ParamName=title]
ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'isbnText') [ParamName=isbnText]
ArgumentOutOfRangeException: copies ('0') must be a non-negative and non-zero value. (Parameter 'copies') | Actual value was 0. [ParamName=copies]
Dune
BookNotFoundException: No book with ISBN 9781234567897 is in the catalog. [IsbnText=9781234567897]

Notes on the example:

  • ArgumentNullException.ThrowIfNull exists since .NET 6; ArgumentException.ThrowIfNullOrWhiteSpace, ArgumentOutOfRangeException.ThrowIfNegativeOrZero and the comparison helpers such as ThrowIfGreaterThan since .NET 8. The older form is If title Is Nothing Then Throw New ArgumentNullException(NameOf(title)).

  • The message of the ArgumentOutOfRangeException has two lines on .NET 10 (the example replaces the line break with a bar).

  • The parameter in the constructor is named isbnText and the property IsbnText. Visual Basic is case-insensitive, so the assignment is written Me.IsbnText = isbnText; plain IsbnText = isbnText would assign the parameter to itself.

No serialization constructor needed

Older exception templates add <Serializable> and a Protected New(info As SerializationInfo, context As StreamingContext) constructor. Formatter-based serialization is obsolete, and on .NET 10 that constructor produces a warning (SYSLIB0051), which is an error when warnings are errors. Leave it out.

Option Strict On
Imports System.Runtime.Serialization

<Serializable>
Public Class OldStyleException
    Inherits Exception

    Public Sub New(message As String)
        MyBase.New(message)
    End Sub

    Protected Sub New(info As SerializationInfo, context As StreamingContext)
        MyBase.New(info, context)
    End Sub
End Class

Module Program
    Sub Main()
        Console.WriteLine(New OldStyleException("legacy shape").Message)
    End Sub
End Module

Built without TreatWarningsAsErrors. The compiler printed:

Program.vb(13,9): warning SYSLIB0051: 'Protected Overloads Sub New(info As SerializationInfo, context As StreamingContext)' is obsolete: 'This API supports obsolete formatter-based serialization. It should not be called or extended by application code.'. (https://aka.ms/dotnet-warnings/SYSLIB0051)
legacy shape

Global handlers and logging

An exception that no Catch handles is unhandled and, in a .NET process, ends the process after the runtime has given the application one chance to log it. Global handlers are for logging and for a clean shutdown (or, in a UI, for keeping the window open), not for routine error handling: after an AppDomain.UnhandledException the process terminates whatever the handler does.

Hook Fires for Where it applies

AppDomain.CurrentDomain.UnhandledException

an exception not caught on any thread; the process ends afterwards (IsTerminating is True)

every application type

AppDomain.CurrentDomain.FirstChanceException

every exception when first thrown, before any handler is searched; observation only

every application type

TaskScheduler.UnobservedTaskException

a faulted Task that nobody awaited, observed when the task is finalized; SetObserved() marks it handled

every application type

Application.ThreadException

an exception from a Windows Forms event handler or message-loop callback on the UI thread

Windows Forms

My.Application.UnhandledException

the same, in a Windows Forms project that uses the VB Application Framework; e.ExitApplication = False keeps the app running

Windows Forms with the Application Framework

Dispatcher.UnhandledException

an exception on a WPF dispatcher thread

WPF

The console program below registers three of the hooks, handles one exception normally, leaves a faulted task unobserved and then throws an exception that nothing catches. The runtime prints its own report after the handler finishes and the process aborts (exit code 134 on Linux; Windows reports a different code).

Option Strict On
Imports System.Runtime.CompilerServices
Imports System.Runtime.ExceptionServices

Module Program
    <MethodImpl(MethodImplOptions.NoInlining)>
    Private Sub FaultAndForget()
        Task.Run(Sub() Throw New TimeoutException("task nobody awaited"))
        Threading.Thread.Sleep(200)
    End Sub

    Sub Main()
        AddHandler AppDomain.CurrentDomain.FirstChanceException,
            Sub(sender As Object, e As FirstChanceExceptionEventArgs)
                Console.WriteLine("FirstChance : " & e.Exception.GetType().Name)
            End Sub

        AddHandler AppDomain.CurrentDomain.UnhandledException,
            Sub(sender As Object, e As UnhandledExceptionEventArgs)
                Dim ex As Exception = DirectCast(e.ExceptionObject, Exception)
                Console.WriteLine("Unhandled   : " & ex.Message & " (IsTerminating=" & e.IsTerminating & ")")
            End Sub

        AddHandler TaskScheduler.UnobservedTaskException,
            Sub(sender As Object, e As UnobservedTaskExceptionEventArgs)
                Console.WriteLine("Unobserved  : " & e.Exception.InnerException.Message)
                e.SetObserved()
            End Sub

        Try
            Throw New FormatException("handled one")
        Catch ex As FormatException
            Console.WriteLine("Caught      : " & ex.Message)
        End Try

        FaultAndForget()
        GC.Collect()
        GC.WaitForPendingFinalizers()

        Console.WriteLine("Throwing an exception nobody catches...")
        Throw New InvalidOperationException("nobody catches this")
    End Sub
End Module
FirstChance : FormatException
Caught      : handled one
FirstChance : TimeoutException
FirstChance : TimeoutException
Unobserved  : task nobody awaited
Throwing an exception nobody catches...
FirstChance : InvalidOperationException
Unhandled   : nobody catches this (IsTerminating=True)
Unhandled exception. System.InvalidOperationException: nobody catches this
   at ShelfDocs.Program.Main() in ex10.vb:line 41
(process exit code 134)

The unobserved-task line appears only because the example allocates the faulted task in a separate non-inlined method and forces a collection; in a real program it appears at some later garbage collection. The stack line of the final report is abbreviated here.

Windows Forms

Windows Forms catches an exception thrown in an event handler and, by default, shows the standard "unhandled exception" dialog. Two ways to take over, both built on the .NET 10 SDK with -p:EnableWindowsTargeting=true but not run, because Windows Forms needs Windows.

With the Application Framework (MyType is WindowsForms; the project has a startup form and no Sub Main of its own), the My.Application.UnhandledException event is available. Put the handler in a partial MyApplication class in the My namespace, conventionally in a file named ApplicationEvents.vb:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <RootNamespace>ShelfDocs</RootNamespace>
    <UseWindowsForms>true</UseWindowsForms>
    <MyType>WindowsForms</MyType>
    <StartupObject>ShelfDocs.MainForm</StartupObject>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnableWindowsTargeting>true</EnableWindowsTargeting>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <Import Include="System.Windows.Forms" />
  </ItemGroup>
</Project>
Option Strict On

Public Class MainForm
    Inherits Form

    Public Sub New()
        Text = "Shelf"
        Dim renew As New Button() With {.Text = "Renew", .Dock = DockStyle.Top}
        AddHandler renew.Click, AddressOf OnRenewClick
        Controls.Add(renew)
    End Sub

    Private Sub OnRenewClick(sender As Object, e As EventArgs)
        Throw New InvalidOperationException("renewal service offline")
    End Sub
End Class
Option Strict On

Namespace My
    Partial Friend Class MyApplication
        Private Sub MyApplication_UnhandledException(sender As Object, e As ApplicationServices.UnhandledExceptionEventArgs) Handles Me.UnhandledException
            MessageBox.Show("Unexpected problem: " & e.Exception.Message, "Shelf")
            e.ExitApplication = False   ' keep the window open
        End Sub
    End Class
End Namespace

With your own Sub Main (MyType is WindowsFormsWithCustomSubMain), the Application Framework and its events are not there. Register Application.ThreadException yourself and call Application.SetUnhandledExceptionMode before creating any form; register the AppDomain event for exceptions on other threads:

Option Strict On
Imports System.IO

Module Program
    Private Sub OnThreadException(sender As Object, e As Threading.ThreadExceptionEventArgs)
        MessageBox.Show("UI thread problem: " & e.Exception.Message, "Shelf")
    End Sub

    Private Sub OnUnhandled(sender As Object, e As UnhandledExceptionEventArgs)
        Dim ex As Exception = DirectCast(e.ExceptionObject, Exception)
        File.AppendAllText(Path.Combine(Path.GetTempPath(), "shelf-crash.log"), ex.ToString() & Environment.NewLine)
    End Sub

    Sub Main()
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)
        AddHandler Application.ThreadException, AddressOf OnThreadException
        AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf OnUnhandled
        Application.EnableVisualStyles()
        Application.Run(New MainForm())
    End Sub
End Module

Public Class MainForm
    Inherits Form

    Public Sub New()
        Dim renew As New Button() With {.Text = "Renew", .Dock = DockStyle.Top}
        AddHandler renew.Click, Sub(sender As Object, e As EventArgs) Throw New InvalidOperationException("renewal service offline")
        Controls.Add(renew)
    End Sub
End Class

Both projects built with 0 warnings and 0 errors. See Windows Forms Applications and The My Namespace for the rest of the Application Framework.

Logging

Log an exception once, where it is handled, with the whole exception object, not just ex.Message: the logger renders the type, the message, the inner exceptions and the stack trace, and structured loggers keep the exception as a field. Microsoft.Extensions.Logging is the abstraction used by ASP.NET Core, the generic host and many libraries; sinks such as Serilog and NLog plug in behind it. The example uses the console provider from the Microsoft.Extensions.Logging.Console package (version 10.0.0, restored from NuGet), and a message template with named placeholders rather than string concatenation.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>ShelfDocs</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
  </ItemGroup>
</Project>
Option Strict On
Imports Microsoft.Extensions.Logging

Public Class LoanService
    Private ReadOnly _logger As ILogger(Of LoanService)

    Public Sub New(logger As ILogger(Of LoanService))
        _logger = logger
    End Sub

    Public Sub Renew(loanId As Integer, extraDays As Integer)
        Try
            ArgumentOutOfRangeException.ThrowIfGreaterThan(extraDays, 14)
            _logger.LogInformation("Loan {LoanId} renewed by {ExtraDays} days", loanId, extraDays)
        Catch ex As ArgumentOutOfRangeException
            _logger.LogError(ex, "Could not renew loan {LoanId}", loanId)
            Throw
        End Try
    End Sub
End Class

Module Program
    Sub Main()
        Using factory As ILoggerFactory = LoggerFactory.Create(
            Sub(builder) builder.SetMinimumLevel(LogLevel.Information).AddSimpleConsole())
            Dim service As New LoanService(factory.CreateLogger(Of LoanService)())
            service.Renew(7, 7)
            Try
                service.Renew(8, 30)
            Catch ex As ArgumentOutOfRangeException
                System.Console.WriteLine("caller still sees: " & ex.GetType().Name)
            End Try
        End Using
    End Sub
End Module
info: ShelfDocs.LoanService[0]
      Loan 7 renewed by 7 days
caller still sees: ArgumentOutOfRangeException
fail: ShelfDocs.LoanService[0]
      Could not renew loan 8
      System.ArgumentOutOfRangeException: extraDays ('30') must be less than or equal to '14'. (Parameter 'extraDays')
      Actual value was 30.
         at System.ArgumentOutOfRangeException.ThrowGreater[T](T value, T other, String paramName)
         at System.ArgumentOutOfRangeException.ThrowIfGreaterThan[T](T value, T other, String paramName)
         at ShelfDocs.LoanService.Renew(Int32 loanId, Int32 extraDays) in ex13.vb:line 13

Source: Logging in .NET

Two details. The LogError(ex, template, args) overload takes the exception first. And the Throw after logging rethrows to the caller; the outcome is a logged and still-propagating exception, which is right when this layer cannot recover. Logging and then swallowing is only right at a boundary that really handles the failure. The console provider writes on a background thread, so the order of its lines relative to Console.WriteLine is not guaranteed (here caller still sees came out before the fail entry). Because the logging namespace contains a child namespace called Console, the program qualifies System.Console.WriteLine; with Imports Microsoft.Extensions.Logging an unqualified Console.WriteLine fails with BC30456, because Console now binds to Microsoft.Extensions.Logging.Console.

Exceptions in asynchronous code

Exceptions thrown inside an Async method are stored in the returned Task, and Await rethrows them with their original type at the awaiting line. Task.WhenAll collects several failures, but Await rethrows only the first; the full list is in Task.Exception. Blocking with Wait() or .Result wraps everything in an AggregateException.

Option Strict On

Module Program
    Private Async Function LookupAsync(isbnText As String) As Task(Of String)
        Await Task.Delay(10)
        Throw New KeyNotFoundException("no book " & isbnText)
    End Function

    Private Async Function FailAfterAsync(ms As Integer, label As String) As Task
        Await Task.Delay(ms)
        Throw New InvalidOperationException(label)
    End Function

    Private Async Function RunAsync() As Task
        Try
            Dim title As String = Await LookupAsync("9780306406157")
            Console.WriteLine(title)
        Catch ex As KeyNotFoundException
            Console.WriteLine("Await rethrows the original type: " & ex.Message)
        End Try

        Dim both As Task = Task.WhenAll(FailAfterAsync(10, "first"), FailAfterAsync(30, "second"))
        Try
            Await both
        Catch ex As InvalidOperationException
            Console.WriteLine("Await keeps only the first: " & ex.Message)
            Console.WriteLine("Task.Exception has them all: " & both.Exception.InnerExceptions.Count)
        End Try

        Try
            both.Wait()
        Catch ex As AggregateException
            Console.WriteLine("Wait wraps them: " & ex.GetType().Name & " with " & ex.InnerExceptions.Count & " inner exceptions")
        End Try
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
Await rethrows the original type: no book 9780306406157
Await keeps only the first: first
Task.Exception has them all: 2
Wait wraps them: AggregateException with 2 inner exceptions

Source: Await Operator

Visual Basic has a restriction that C# no longer has: Await cannot appear inside a Catch block, a Finally block or a SyncLock (BC36943). Move the awaited call after the Try statement, record the outcome in a local variable, or restructure the loop.

Option Strict On

Module Program
    Private Async Function RunAsync() As Task
        Try
            Throw New InvalidOperationException("in Try")
        Catch ex As InvalidOperationException
            Await Task.Delay(1)
        Finally
            Await Task.Delay(1)
        End Try
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
Program.vb(8,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.
Program.vb(10,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.

More on Task, cancellation and Async Sub on Async and Await.

Differences from C#

Topic Visual Basic C#

Syntax

Try ... Catch ex As T When ... Finally ... End Try

try { } catch (T ex) when (...) { } finally { }

Rethrow

Throw keeps the trace; Throw ex restarts it (CA2200 warning)

throw; keeps the trace; throw ex; restarts it

Throw in an expression

not possible (BC30201)

x ?? throw ..., cond ? a : throw ... since C# 7.0

Await in Catch/Finally

not allowed (BC36943)

allowed since C# 6

Leaving Finally

BC30101

CS0157

Unreachable catch order

BC42029 warning

CS0160 error

Exception variable

declared by As in each Catch

declared in the parentheses

Overflow

checked by default: Integer.MaxValue + 1 throws OverflowException

unchecked by default; use checked

/ on integers

returns Double, so division by zero gives Infinity

integer division, throws DivideByZeroException

Text to number

CInt("12x") throws InvalidCastException; Integer.Parse throws FormatException

int.Parse throws FormatException

Legacy model

On Error, Resume, Err

none

Compiler errors and warnings

All of these were produced by the SDK on this page or on Unstructured Error Handling: On Error.

Code Message (abridged) Cause

BC30030

Try must have at least one 'Catch' or a 'Finally'.

a Try with neither

BC30101

Branching out of a 'Finally' is not valid.

Return/Exit/GoTo out of Finally

BC30201

Expression expected.

Throw used where a value is required

BC30381

'Finally' can only appear once in a 'Try' statement.

two Finally blocks

BC30392

'Catch' cannot catch type 'String' because it is not 'System.Exception' or a class that inherits from 'System.Exception'.

non-exception type in Catch

BC30393

'Exit Try' can only appear inside a 'Try' statement.

Exit Try outside Try

BC30451

'x' is not declared. It may be inaccessible due to its protection level.

a Try-block local used in Catch/Finally, or Catch ex without As

BC30456

'WriteLine' is not a member of 'Microsoft.Extensions.Logging.Console'.

Console bound to a namespace

BC30542

Division by zero occurred while evaluating this expression.

constant \ or Mod by zero

BC30616

Variable 'ex' hides a variable in an enclosing block.

a Catch variable named like another local

BC30665

'Throw' operand must derive from 'System.Exception'.

Throw "text"

BC30666

'Throw' statement cannot omit operand outside a 'Catch' statement or inside a 'Finally' statement.

bare Throw where there is nothing to rethrow

BC30754

'GoTo Inside' is not valid because 'Inside' is inside a 'Try', 'Catch' or 'Finally' statement that does not contain this statement.

jumping into a Try

BC36943

'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.

Await in a handler

BC42029

'Catch' block never reached, because 'ArgumentException' inherits from 'Exception'.

catch order (warning)

CA2200

Re-throwing caught exception changes stack information

Throw ex (warning)

SYSLIB0051

…​ is obsolete: 'This API supports obsolete formatter-based serialization…​'

serialization constructor (warning)

The remaining codes in the table came from five more failing programs, each a one- or two-statement mistake: a Try with no handler (BC30030), a local declared in the Try block and used in Catch and Finally (BC30451), a GoTo into a Try (BC30754), a variable named ex declared again inside a Catch ex As ... block (BC30616), and two Finally blocks (BC30381). The messages are copied from the compiler output.

Option Strict On

Module Program
    Sub Main()
        Try
            Console.WriteLine("a")
        End Try
    End Sub
End Module
Program.vb(5,9): error BC30030: Try must have at least one 'Catch' or a 'Finally'.
Option Strict On

Module Program
    Sub Main()
        Try
            Dim opened As Integer = 1
        Catch ex As Exception
            Console.WriteLine(opened)
        Finally
            Console.WriteLine(opened)
        End Try
    End Sub
End Module
Program.vb(8,31): error BC30451: 'opened' is not declared. It may be inaccessible due to its protection level.
Program.vb(10,31): error BC30451: 'opened' is not declared. It may be inaccessible due to its protection level.
Option Strict On

Module Program
    Sub Main()
        GoTo Inside
        Try
Inside:
            Console.WriteLine("a")
        Catch ex As Exception
        End Try
    End Sub
End Module
Program.vb(5,14): error BC30754: 'GoTo Inside' is not valid because 'Inside' is inside a 'Try', 'Catch' or 'Finally' statement that does not contain this statement.
Option Strict On

Module Program
    Sub Main()
        Try
            Console.WriteLine("a")
        Catch ex As ArgumentException
            Dim ex As Integer = 3
        End Try
        Dim e As Exception
        Try
        Catch e As Exception
        End Try
    End Sub
End Module
Program.vb(8,17): error BC30616: Variable 'ex' hides a variable in an enclosing block.
Program.vb(10,13): warning BC42024: Unused local variable: 'e'.
Program.vb(12,15): error BC30616: Variable 'e' hides a variable in an enclosing block.
Option Strict On

Module Program
    Sub Main()
        Try
            Console.WriteLine("a")
        Catch ex As Exception
        Catch ex As ArgumentException When ex.Message = "x"
        Finally
        Finally
        End Try
    End Sub
End Module
Program.vb(10,9): error BC30381: 'Finally' can only appear once in a 'Try' statement.

The compiler stops reporting semantic errors in a body once it has found a syntax error, which is why Finally twice (BC30381) hides the other mistake in that program. Fix and rebuild.

Where introductory material is out of date

Introductory VB.NET books and tutorials, including popular ones, were mostly written for earlier Visual Studio versions. For exceptions, check these points when you meet them:

  • 10 / 0 does not throw: it is Infinity. Only \, Mod and Decimal division throw DivideByZeroException.

  • ApplicationException as the base of custom exceptions is outdated advice; derive from Exception.

  • A serialization constructor and <Serializable> on custom exceptions are no longer needed and warn on .NET 10 (SYSLIB0051).

  • Catch order matters, and a base type first produces BC42029, not a working program.

  • CInt, CDbl and CDate on text throw InvalidCastException, not FormatException. Prefer the TryParse methods.

  • Logging with Debug.WriteLine or a text file is replaced by Microsoft.Extensions.Logging (or a framework behind it).

  • A global handler cannot keep a console or service process alive after AppDomain.UnhandledException; only a UI framework event such as Application.ThreadException can.

  • On Error is not how new Visual Basic is written; see On Error.

Version notes and pitfalls

Topic Note

Try/Catch/Finally, Throw

available since the first .NET version of Visual Basic; the replacement for the VB6 On Error model

When filters

compile to CLR exception filters (observable in the first-pass behaviour above)

Throw helpers

ThrowIfNull since .NET 6; ThrowIfNullOrWhiteSpace, ThrowIfNegativeOrZero, ThrowIfGreaterThan since .NET 8

Obsolete serialization constructor

SYSLIB0051 on current .NET versions

Windows-only hooks

Application.ThreadException and My.Application.UnhandledException are Windows Forms; WPF has Dispatcher events

.NET Framework

the same Try model; Microsoft.Extensions.Logging is a NuGet package there; the Application Framework hooks are the same

Pitfalls:

  • Catching Exception and doing nothing hides defects. Log and rethrow, or handle a specific type.

  • Throw ex loses the trace; use Throw.

  • Messages are for people. Do not parse ex.Message; branch on the type, on HResult or on a property.

  • An exception in Finally or Dispose masks the original.

  • Do not catch NullReferenceException, IndexOutOfRangeException or InvalidCastException to cope with bad input; test the input. They indicate defects.

  • Using and Finally do not run when the process is terminated (stack overflow, End, Environment.FailFast).