Error Handling in VB 2026
Write bulletproof applications using Try/Catch/Finally, the Exception class hierarchy, multiple Catch blocks, custom exceptions, structured error logging, and async error handling — so your app recovers gracefully instead of crashing.
Try/Catch/Finally. Catch the most specific exception type first (e.g. FormatException before Exception) — VB uses the first matching Catch block. Put clean-up code in Finally: it always runs, whether an exception was thrown or not, even if a Return statement exits the Try block early. Never swallow exceptions silently with an empty Catch — at minimum log the message. Use ex.Message for user-friendly text and ex.ToString() for full diagnostic output including the stack trace.
24.1 Try / Catch / Finally
A Try block wraps the code that might fail. If an exception is thrown, execution jumps immediately to the first matching Catch block. The Finally block runs regardless — it's the right place to release resources that must be freed whether the code succeeded or failed.
' --- Basic structure --- Try ' Code that might throw Dim n = CInt(txtInput.Text) ' FormatException if not a number Dim result = 100 \ n ' DivideByZeroException if n = 0 lblResult.Text = result.ToString() Catch ex As FormatException ' Caught first — most specific type lblResult.Text = "Please enter a whole number." Catch ex As DivideByZeroException lblResult.Text = "Cannot divide by zero." Catch ex As Exception ' Catch-all — least specific, must be last lblResult.Text = $"Unexpected error: {ex.Message}" Finally ' Always runs — even if an exception was thrown or Return was called btnCalculate.Enabled = True lblStatus.Text = "Ready" End Try ' --- Try/Catch without Finally --- Try File.Delete(filePath) Catch ex As IOException MessageBox.Show($"Cannot delete file: {ex.Message}") End Try ' --- Finally is NOT the same as code after End Try --- ' Code after End Try does NOT run if Catch re-throws. ' Finally ALWAYS runs. Try conn.Open() RunQuery(conn) Catch ex As Exception LogError(ex) Throw ' re-throw to caller — but Finally still runs! Finally conn.Close() ' always closes the connection End Try
VB uses the first Catch block whose type matches the thrown exception. Always order from most specific to least specific. Putting Catch ex As Exception first would catch everything — including FormatException and DivideByZeroException — and none of your specific handlers would ever run.
Choose an operation and input. See which Catch block fires, the exact exception type and message, and confirm Finally always runs — even when an exception is thrown.
24.2 The Exception Hierarchy
All exceptions inherit from System.Exception. Knowing which type to catch lets you handle each failure mode precisely. Catching a parent type catches all children too — IOException catches both FileNotFoundException and DirectoryNotFoundException.
| Exception Type | Common Cause | Typical Fix |
|---|---|---|
| FormatException | CInt("abc"), bad date | Validate input; use Integer.TryParse |
| OverflowException | CInt(Long.MaxValue) | Use larger type or check range first |
| DivideByZeroException | x \ 0 (integers) | Guard with If divisor <> 0 |
| NullReferenceException | obj.Method() when obj is Nothing | Check If obj IsNot Nothing first |
| IndexOutOfRangeException | arr(100) on short array | Check i < arr.Length |
| FileNotFoundException | File.ReadAllText(missing path) | Check File.Exists() first |
| InvalidCastException | DirectCast(obj, WrongType) | Use TypeOf … Is or TryCast |
| ArgumentNullException | Passing Nothing to a method | Validate parameters at top of method |
Select an exception type to see: what triggers it, the right Catch syntax, the recommended prevention strategy, and where it sits in the hierarchy.
24.3 Prevention: TryParse and Guard Clauses
The best error handling is not needing it. Validate inputs before they fail rather than catching exceptions after. Integer.TryParse, Double.TryParse, DateTime.TryParse, and File.Exists let you test without exceptions. Guard clauses exit early with meaningful messages.
' --- TryParse: no exception thrown on bad input --- Dim n As Integer If Not Integer.TryParse(txtInput.Text, n) Then lblError.Text = "Please enter a whole number." Return End If ' n is now safely parsed — no Try/Catch needed ' --- TryParse for Double, Decimal, DateTime --- Dim price As Decimal If Not Decimal.TryParse(txtPrice.Text, price) OrElse price < 0 Then ShowError("Price must be a positive number.") Return End If Dim dob As DateTime If Not DateTime.TryParse(txtDob.Text, dob) Then ShowError("Please enter a valid date (dd/MM/yyyy).") Return End If ' --- Guard clauses: validate all inputs at the top --- Private Sub SaveCustomer(name As String, age As Integer, email As String) ' Guards at the top — exit early if invalid If String.IsNullOrWhiteSpace(name) Then Throw New ArgumentException("Name cannot be empty.", "name") End If If age < 0 OrElse age > 150 Then Throw New ArgumentOutOfRangeException("age", $"Age {age} is outside the valid range 0–150.") End If If Not email.Contains("@") Then Throw New ArgumentException("Email address is not valid.", "email") End If ' Safe to proceed db.SaveCustomer(name, age, email) End Sub ' --- File existence check before reading --- If Not File.Exists(filePath) Then MessageBox.Show($"File not found: {filePath}") Return End If Dim content = File.ReadAllText(filePath) ' --- TryCast instead of DirectCast --- Dim btn = TryCast(sender, Button) If btn IsNot Nothing Then btn.Text = "Clicked" ' safe — only runs if cast succeeded End If
A registration form that validates each field using TryParse and guard clauses before attempting to save. See exactly which validation fails and the corresponding VB guard code.
24.4 Custom Exceptions
When built-in exception types don't describe your domain error precisely, create your own. Inherit from Exception (or a more specific base) and add properties that carry domain-specific context. This makes errors self-documenting and lets callers catch your exception type specifically.
' --- Define a custom exception --- Public Class InsufficientStockException Inherits Exception Public ReadOnly Property ProductName As String Public ReadOnly Property Requested As Integer Public ReadOnly Property Available As Integer Public Sub New(productName As String, requested As Integer, available As Integer) MyBase.New($"Insufficient stock for '{productName}': requested {requested}, available {available}.") Me.ProductName = productName Me.Requested = requested Me.Available = available End Sub End Class ' --- Another custom exception --- Public Class ValidationException Inherits Exception Public ReadOnly Property FieldName As String Public Sub New(field As String, message As String) MyBase.New(message) Me.FieldName = field End Sub End Class ' --- Throw and catch custom exceptions --- Private Sub ProcessOrder(productName As String, qty As Integer) Dim stock = GetStock(productName) If qty > stock Then Throw New InsufficientStockException(productName, qty, stock) End If DeductStock(productName, qty) End Sub Private Sub btnCheckout_Click(...) Handles btnCheckout.Click Try ProcessOrder("Widget A", CInt(nudQty.Value)) lblStatus.Text = "Order placed!" Catch ex As InsufficientStockException ' Access domain-specific properties lblStatus.Text = $"Only {ex.Available} units left (you requested {ex.Requested})." nudQty.Maximum = ex.Available Catch ex As Exception LogError(ex) lblStatus.Text = "Unexpected error. Please try again." End Try End Sub ' --- Re-throw with inner exception (wrapping) --- Try db.Save(order) Catch ex As SqlException Throw New OrderSaveException($"Failed to save order #{order.Id}", ex) ' ↑ inner exception End Try
Try ordering more units than available. The custom InsufficientStockException carries ProductName, Requested, and Available properties — shown in the Catch block output.
24.5 Structured Error Logging
Never show raw exception messages to end users, but never discard them either. Log the full details — timestamp, exception type, message, stack trace — to a file or event log so you can diagnose problems after deployment. A simple logger is easy to build in VB.
' --- Simple file logger module --- Module AppLogger Private ReadOnly _logPath As String = Path.Combine(Application.StartupPath, "app.log") Enum LogLevel INFO WARN [ERROR] End Enum Public Sub Log(level As LogLevel, message As String, Optional ex As Exception = Nothing) Dim ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") Dim line = $"[{ts}] [{level,-5}] {message}" If ex IsNot Nothing Then line &= Environment.NewLine & $" Exception: {ex.GetType().Name}: {ex.Message}" line &= Environment.NewLine & $" StackTrace: {ex.StackTrace}" If ex.InnerException IsNot Nothing Then line &= Environment.NewLine & $" InnerException: {ex.InnerException.Message}" End If End If Try File.AppendAllText(_logPath, line & Environment.NewLine) Catch ' If logging itself fails, swallow silently (can't log the logger error) End Try End Sub End Module ' --- Usage --- AppLogger.Log(LogLevel.INFO, "Application started") AppLogger.Log(LogLevel.WARN, $"Config file missing, using defaults") AppLogger.Log(LogLevel.ERROR, $"Order #{orderId} failed", ex) ' --- Global unhandled exception handler --- Sub Main() AddHandler Application.ThreadException, AddressOf OnUnhandledException AppDomain.CurrentDomain.UnhandledException += AddressOf OnUnhandledException Application.Run(New Form1()) End Sub Private Sub OnUnhandledException(sender As Object, e As ThreadExceptionEventArgs) AppLogger.Log(LogLevel.ERROR, "Unhandled exception", e.Exception) MessageBox.Show("An unexpected error occurred. The error has been logged.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) End Sub
Simulate application events that trigger INFO, WARN, and ERROR log entries. Watch the log fill up with timestamped, structured entries exactly as AppLogger.Log() would write them to app.log.
24.6 GitHub Copilot — Error Handling Patterns
' Write a generic Async Function RetryAsync(Of T) that retries a Func(Of Task(Of T)) up to maxRetries times with exponential back-off, catching transient exceptions (IOException, WebException, SqlException). Log each attempt.'' Retry with exponential back-off — handles transient failures. Imports System.Net Imports System.Data.SqlClient Public Async Function RetryAsync(Of T)( operation As Func(Of Task(Of T)), maxRetries As Integer = 3, baseDelayMs As Integer = 500) As Task(Of T) For attempt = 1 To maxRetries Try Return Await operation() ' success — return immediately Catch ex As Exception When IsTransient(ex) AndAlso attempt < maxRetries Dim delay = baseDelayMs * CInt(Math.Pow(2, attempt - 1)) AppLogger.Log(LogLevel.WARN, $"Attempt {attempt}/{maxRetries} failed ({ex.GetType().Name}). Retrying in {delay}ms...", ex) Await Task.Delay(delay) End Try Next ' Final attempt — let the exception propagate Return Await operation() End Function Private Function IsTransient(ex As Exception) As Boolean Return TypeOf ex Is IO.IOException OrElse TypeOf ex Is WebException OrElse TypeOf ex Is SqlException End Function ' Usage: Dim data = Await RetryAsync(Of String)( Async Function() Return Await httpClient.GetStringAsync(apiUrl), maxRetries := 3)
Try these in the Copilot Chat panel while working with error handling:
- "Write a SafeConvert module with functions SafeToInt, SafeToDecimal, SafeToDate that return Nullable(Of T) using TryParse — returning Nothing on failure instead of throwing"
- "Create a custom ValidationException class with a List(Of String) Errors property, and a Validate() Sub that collects all field errors and throws it once at the end"
- "Build a global error handler in Sub Main using Application.ThreadException and AppDomain.CurrentDomain.UnhandledException that logs to a rolling daily file (app_2026-02-24.log)"
- "Write a unit test using MSTest that asserts a specific custom exception type and its properties are thrown by ProcessOrder when stock is insufficient"
Lesson Summary
- Wrap risky code in
Try/Catch/Finally. Order Catch blocks from most specific to least specific — VB uses the first matching Catch. Finallyalways executes — whether the Try block succeeded, threw, or usedReturn. Use it for resource cleanup: close files, connections, and locks.- Use
ex.Messagefor user messages. Useex.ToString()(includes stack trace) for log files. Never show raw stack traces to end users. - Prevent exceptions before they happen:
Integer.TryParse,File.Exists,TryCast, and guard clauses at the top of methods are cleaner than catching avoidable errors. - Create custom exception classes by inheriting from
Exception. Add domain-specific properties (e.g.ProductName,Available) so callers get structured error information. - Never swallow exceptions silently. Log at minimum the timestamp, level, message, exception type and stack trace. Use a structured logger module with
File.AppendAllText. - Register a global handler with
Application.ThreadExceptionto catch any unhandled exceptions and show a friendly message instead of letting the app crash.
Exercises
Exercise 24.1 — Safe Calculator
- Build a calculator with two text inputs and an operator dropdown (+, −, ×, ÷, \, Mod, ^)
- Use
Decimal.TryParsefor input validation — no Try/Catch for parsing - Wrap the operation in Try/Catch catching
DivideByZeroException,OverflowException, andExceptionseparately - Show a friendly message per error type; log the full exception to a ListBox "log" panel
- Copilot challenge: Ask Copilot to "add expression history: save each successful calculation to a List(Of String) and display in a ListBox; double-click to re-run"
Exercise 24.2 — File Reader with Logging
- Let the user browse for a text file; read it with
File.ReadAllTextinside Try/Catch - Catch
FileNotFoundException,UnauthorizedAccessException, andIOExceptionseparately with specific user messages - Always close the file (use
FinallyorUsingstatement) - Log every error to a rolling log file named
app_yyyy-MM-dd.log - Copilot challenge: Ask Copilot to "wrap File.ReadAllText in a RetryAsync helper that retries 3 times with 500ms, 1s, 2s delays for IOException"
Exercise 24.3 — Order Validation System
- Create
OrderException,InsufficientStockException, andInvalidProductExceptioncustom exception classes with domain properties - Write a
ProcessOrderSub that validates product name, quantity (1–999), and stock level before completing - Catch each custom type separately in the button handler and show tailored error messages
- Add a global handler in Sub Main that logs unhandled exceptions and shows a user-friendly dialog
- Copilot challenge: Ask Copilot to "add a ValidationException that accumulates all field errors in a List(Of String) and throws once at the end of validation"
Related Resources
← Lesson 23
Web Browser — WebView2 control.
Lesson 25 →
OOP — Classes, Inheritance, Polymorphism.
MS Docs — Exceptions
Best practices for exceptions in .NET 10.
MS Docs — Exception Class
Full Exception class API reference.
Featured Books
Visual Basic 2022 Made Easy
Error handling, file I/O, and robust application patterns covered with complete programs.
View on Amazon →
VB Programming With Code Examples
Real-world programs using structured error handling and validation throughout.
View on Amazon →