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 |
|---|---|---|
|
run code, handle selected exceptions, always run cleanup |
this page |
|
catch only when a condition on the exception holds (a CLR exception filter) |
this page |
|
raise an exception, or rethrow the one being handled |
this page |
|
leave the |
this page |
|
dispose a resource even when an exception is thrown |
|
|
the VB6 model, kept for compatibility |
|
|
exceptions travel in the returned |
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).
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'
Source: Try…Catch…Finally Statement
The rules in one place:
| Rule | Detail |
|---|---|
Matching |
a |
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 |
|
runs when the |
Locals |
a variable declared inside the |
Result |
a |
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
Source: Try…Catch…Finally Statement
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
Finallyblocks of inner frames run. In the first case below, the filter prints beforeinner 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
Falseobserves every exception without ever catching it (case 4). Compare this withCatch ex As Exceptionfollowed byThrow, 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
Source: Try…Catch…Finally Statement
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
Finallyreplaces the one in flight. The original is lost unless you wrap it as theInnerExceptionof the new one. SoFinallycode should be small and should not throw;Disposeimplementations must not throw. -
Control cannot leave a
Finallyblock:Return,Exit For,Exit Do,GoToandExit Subout 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
Source: System.Exception class
| Member | Notes |
|---|---|
|
human-readable description; for the user or the log, not for program logic |
|
|
|
the exception that caused this one; walk it in a loop, or use |
|
an |
|
a number; |
|
assembly name and the method that threw |
|
a URL or file for documentation; rarely used |
|
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
Source: Exception class hierarchy
Three consequences:
-
FileNotFoundExceptionderives fromIOException, andObjectDisposedExceptionfromInvalidOperationException, so order matters when catching both a family and a member. -
ArgumentNullExceptionandArgumentOutOfRangeExceptionderive fromArgumentException;DivideByZeroExceptionandOverflowExceptionfromArithmeticException. -
OperationCanceledExceptionis not anIOExceptionor anArgumentException; 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 |
|---|---|---|
|
member access on |
test with |
|
|
|
|
array or list index beyond the end; VB array bounds are upper bounds, so |
check |
|
|
|
|
changing a collection while a |
iterate a copy or by index; check state first |
|
|
|
|
using an object after |
scope with |
|
file system access |
catch these (they can happen even after an existence check); see Files, Directories and Streams |
|
modifying a read-only collection, unsupported operation |
choose the right collection type |
|
|
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)
Source: StackOverflowException class
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.Averageof an empty list is a design decision (zero,Nothing, an error), not a case forTry. -
Do not use exceptions for ordinary control flow. The
Trypattern (Integer.TryParse,Dictionary.TryGetValue, the shelf’sIsbn.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
Source: Best practices for exceptions
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]
Source: Create user-defined exceptions
Notes on the example:
-
ArgumentNullException.ThrowIfNullexists since .NET 6;ArgumentException.ThrowIfNullOrWhiteSpace,ArgumentOutOfRangeException.ThrowIfNegativeOrZeroand the comparison helpers such asThrowIfGreaterThansince .NET 8. The older form isIf title Is Nothing Then Throw New ArgumentNullException(NameOf(title)). -
The message of the
ArgumentOutOfRangeExceptionhas two lines on .NET 10 (the example replaces the line break with a bar). -
The parameter in the constructor is named
isbnTextand the propertyIsbnText. Visual Basic is case-insensitive, so the assignment is writtenMe.IsbnText = isbnText; plainIsbnText = isbnTextwould 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 |
|---|---|---|
|
an exception not caught on any thread; the process ends afterwards ( |
every application type |
|
every exception when first thrown, before any handler is searched; observation only |
every application type |
|
a faulted |
every application type |
|
an exception from a Windows Forms event handler or message-loop callback on the UI thread |
Windows Forms |
|
the same, in a Windows Forms project that uses the VB Application Framework; |
Windows Forms with the Application Framework |
|
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 |
|
|
Rethrow |
|
|
Throw in an expression |
not possible (BC30201) |
|
|
not allowed (BC36943) |
allowed since C# 6 |
Leaving |
BC30101 |
CS0157 |
Unreachable catch order |
BC42029 warning |
CS0160 error |
Exception variable |
declared by |
declared in the parentheses |
Overflow |
checked by default: |
unchecked by default; use |
|
returns |
integer division, throws |
Text to number |
|
|
Legacy model |
|
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 |
BC30101 |
Branching out of a 'Finally' is not valid. |
|
BC30201 |
Expression expected. |
|
BC30381 |
'Finally' can only appear once in a 'Try' statement. |
two |
BC30392 |
'Catch' cannot catch type 'String' because it is not 'System.Exception' or a class that inherits from 'System.Exception'. |
non-exception type in |
BC30393 |
'Exit Try' can only appear inside a 'Try' statement. |
|
BC30451 |
'x' is not declared. It may be inaccessible due to its protection level. |
a |
BC30456 |
'WriteLine' is not a member of 'Microsoft.Extensions.Logging.Console'. |
|
BC30542 |
Division by zero occurred while evaluating this expression. |
constant |
BC30616 |
Variable 'ex' hides a variable in an enclosing block. |
a |
BC30665 |
'Throw' operand must derive from 'System.Exception'. |
|
BC30666 |
'Throw' statement cannot omit operand outside a 'Catch' statement or inside a 'Finally' statement. |
bare |
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 |
BC36943 |
'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement. |
|
BC42029 |
'Catch' block never reached, because 'ArgumentException' inherits from 'Exception'. |
catch order (warning) |
CA2200 |
Re-throwing caught exception changes stack information |
|
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 / 0does not throw: it isInfinity. Only\,ModandDecimaldivision throwDivideByZeroException. -
ApplicationExceptionas the base of custom exceptions is outdated advice; derive fromException. -
A serialization constructor and
<Serializable>on custom exceptions are no longer needed and warn on .NET 10 (SYSLIB0051). -
Catchorder matters, and a base type first produces BC42029, not a working program. -
CInt,CDblandCDateon text throwInvalidCastException, notFormatException. Prefer theTryParsemethods. -
Logging with
Debug.WriteLineor a text file is replaced byMicrosoft.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 asApplication.ThreadExceptioncan. -
On Erroris not how new Visual Basic is written; see On Error.
Version notes and pitfalls
| Topic | Note |
|---|---|
|
available since the first .NET version of Visual Basic; the replacement for the VB6 |
|
compile to CLR exception filters (observable in the first-pass behaviour above) |
Throw helpers |
|
Obsolete serialization constructor |
SYSLIB0051 on current .NET versions |
Windows-only hooks |
|
.NET Framework |
the same |
Pitfalls:
-
Catching
Exceptionand doing nothing hides defects. Log and rethrow, or handle a specific type. -
Throw exloses the trace; useThrow. -
Messages are for people. Do not parse
ex.Message; branch on the type, onHResultor on a property. -
An exception in
FinallyorDisposemasks the original. -
Do not catch
NullReferenceException,IndexOutOfRangeExceptionorInvalidCastExceptionto cope with bad input; test the input. They indicate defects. -
UsingandFinallydo not run when the process is terminated (stack overflow,End,Environment.FailFast).
References
-
Try…Catch…Finally Statement, Throw Statement and Exit Statement
-
Handling and throwing exceptions in .NET, Best practices for exceptions, Exception class hierarchy and Create user-defined exceptions
-
System.Exception class and Using user-filtered exception handlers
-
AppDomain.UnhandledException, AppDomain.FirstChanceException, TaskScheduler.UnobservedTaskException, Application.ThreadException and WindowsFormsApplicationBase.UnhandledException