Collections and Exception Handling
Before you start
You need: classes (Article 04).
Time: about 45 minutes, plus the practice.
Learning objective
Choose the right collection for a task, and read, fix, or replace any error-handling style you meet in an existing VB.NET application.
Topics
ArrayListandHashtableversusList(Of T)andDictionary(Of TKey, TValue)- Why untyped collections are a defect under
Option Strict Off Try,Catch,Finally,ThrowCatch ... Whenfilters- Exception types worth catching, and the ones to leave alone
On Error GoToandOn Error Resume NextUsingand deterministic cleanup
Legacy collections
Pre-generics VB.NET used ArrayList, Hashtable, and Collection. All store Object, so everything goes in boxed and comes out untyped.
' Legacy — compiles, and hides a bug
Dim students As New ArrayList()
students.Add(New Student("Ravi Kumar", "NCA-2024-0012"))
students.Add("this is not a Student")
For Each item As Object In students
Dim student As Student = CType(item, Student) ' throws on the second item
Next
Under Option Strict Off this is worse: students(0).Name compiles through late binding, resolves at run time, and fails only when the data is wrong.
The generic equivalents are type-safe and faster, because no boxing occurs:
Dim students As New List(Of Student)()
students.Add(New Student("Ravi Kumar", "NCA-2024-0012"))
' students.Add("text") <- now a compile error
Dim byRoll As New Dictionary(Of String, Student)()
byRoll("NCA-2024-0012") = students(0)
| Legacy | Generic replacement | Note |
|---|---|---|
ArrayList | List(Of T) | Direct replacement |
Hashtable | Dictionary(Of TKey, TValue) | Missing key: Hashtable returns Nothing, Dictionary throws |
SortedList | SortedList(Of TKey, TValue) | |
Queue / Stack | Queue(Of T) / Stack(Of T) | |
Collection | List(Of T) | VB6 holdover, one-based indexing |
Two of those rows cause real bugs during modernisation.
Microsoft.VisualBasic.Collection indexes from 1, not 0. Code that reads items(1) for the first element breaks silently when swapped for List(Of T).
Hashtable returns Nothing for a missing key; Dictionary raises KeyNotFoundException. Legacy code written against Hashtable often tests the result for Nothing and treats it as "not found". After a swap, that path throws instead. Use TryGetValue:
Dim student As Student = Nothing
If byRoll.TryGetValue(rollNumber, student) Then
Process(student)
Else
ShowMessage("No student with roll number " & rollNumber & ".")
End If
Common collection operations
Dim activeStudents As List(Of Student) = students.
Where(Function(s) s.Status = StudentStatus.Active).
OrderBy(Function(s) s.ClassName).
ThenBy(Function(s) s.Name).
ToList()
Dim topper As Student = results.
Where(Function(r) Not r.IsAbsent).
OrderByDescending(Function(r) r.MarksObtained).
FirstOrDefault()
LINQ works in VB.NET with Function(x) lambdas in place of C#'s x =>. VB.NET also has query syntax, which appears in some codebases:
Dim names = From s In students
Where s.Status = StudentStatus.Active
Order By s.Name
Select s.Name
FirstOrDefault returns Nothing for a reference type when nothing matches — always test the result before using it.
Structured exception handling
Public Function LoadStudent(rollNumber As String) As Student
Try
Return _studentRepository.FindByRollNumber(_schoolId, rollNumber)
Catch ex As SqlException
_logger.Error("Database failure loading " & rollNumber, ex)
Throw New DataAccessException("Student lookup failed.", ex)
Catch ex As ArgumentException
_logger.Warn("Invalid roll number: " & rollNumber, ex)
Return Nothing
Finally
_stopwatch.Stop()
End Try
End Function
Catch blocks are tested in order, so list the most specific type first. A Catch ex As Exception placed before Catch ex As SqlException makes the second block unreachable — the compiler warns, and the warning is often ignored in legacy projects.
Finally always runs, including when the Try block returns.
Rethrowing correctly
Catch ex As SqlException
_logger.Error("Failed", ex)
Throw ' correct — preserves the original stack trace
End Try
Catch ex As SqlException
_logger.Error("Failed", ex)
Throw ex ' wrong — resets the stack trace to this line
End Try
Throw ex discards where the exception actually came from. When a production stack trace points at the Catch block instead of the failing line, this is why.
Catch filters
VB.NET has had exception filters since 2005, long before C# gained them.
Catch ex As SqlException When ex.Number = 2627
' 2627 is a unique-constraint violation
Return SaveResult.DuplicateRollNumber
Catch ex As SqlException When ex.Number = -2
Return SaveResult.Timeout
End Try
A filter is evaluated before the stack unwinds, so the original state is still intact when debugging. Filtering is better than catching broadly and rethrowing.
What not to catch
' Almost always wrong
Catch ex As Exception
' swallowed — the caller has no idea anything failed
End Try
An empty Catch block is the most damaging pattern in legacy code because the application appears to work while producing wrong results. When you find one, do not simply delete it — find out what it was hiding first.
Catch a specific exception you can actually handle. Let everything else travel up to a handler that logs it.
On Error — the VB6 inheritance
VB.NET still supports unstructured error handling for backward compatibility. You will meet it in migrated code.
Public Sub SaveStudent(student As Student)
On Error GoTo ErrorHandler
_repository.Save(student)
Exit Sub
ErrorHandler:
LogError(Err.Number, Err.Description)
Resume Next
End Sub
Err is a global object holding the last error. Resume Next continues at the statement after the failure; Resume retries the failing statement.
Far worse is the blanket form:
On Error Resume Next
This ignores every error in the procedure and continues to the next line. A failed database call leaves an object Nothing, the next line dereferences it, that error is also ignored, and the procedure finishes "successfully" having done nothing. This is a leading cause of silent data corruption in legacy applications.
Rules for working with it:
On ErrorandTry/Catchcannot be mixed in the same procedure — the compiler rejects it- Convert one procedure at a time, and test it
- Treat
On Error Resume Nextas a defect, not a style - Before removing it, check the log for what was being suppressed; some procedures depend on the tolerance
Conversion is usually direct:
Public Sub SaveStudent(student As Student)
Try
_repository.Save(student)
Catch ex As SqlException
LogError(ex)
Throw
End Try
End Sub
Using and cleanup
Anything holding an unmanaged resource — connections, files, streams, SqlDataReader — implements IDisposable and belongs in a Using block.
Using connection As New SqlConnection(_connectionString)
connection.Open()
Using command As New SqlCommand("SELECT Name FROM Student WHERE Id = @Id", connection)
command.Parameters.Add("@Id", SqlDbType.Int).Value = studentId
Using reader As SqlDataReader = command.ExecuteReader()
While reader.Read()
names.Add(reader.GetString(0))
End While
End Using
End Using
End Using
Using disposes the object even when an exception is thrown, which a manual .Close() at the end of a procedure does not. Connections left open by an early return or an unhandled exception exhaust the connection pool — the application works for an hour, then every request times out.
Several resources can share one block:
Using connection As New SqlConnection(_connectionString),
command As New SqlCommand(sql, connection)
' ...
End Using
Errors you will hit
| Message | Cause | Fix |
|---|---|---|
System.InvalidCastException reading from an ArrayList | Legacy untyped collection holding mixed types | Move to List(Of T) where you can |
BC30456: 'Add' is not a member of 'Hashtable' | Confused Hashtable with Dictionary(Of K,V) | Check which one the code uses |
| Errors vanish and the program continues wrongly | On Error Resume Next | Replace with Try/Catch, one routine at a time |
| Stack trace points at the rethrow, not the failure | Throw ex instead of bare Throw | Use Throw |
System.InvalidOperationException: Collection was modified | Removed inside For Each | Loop backwards with For |
| Connection stays open after an error | No Using block | Wrap it in Using |
On Error Resume Next is the most dangerous line in any legacy file. It does not handle errors — it ignores them, and the program carries on with wrong data.
Common mistakes
- Leaving
ArrayListandHashtablein code that is otherwise modern - Swapping
HashtableforDictionarywithout handling the missing-key throw - Swapping VB6
CollectionforList(Of T)and inheriting an off-by-one from one-based indexing - Empty
Catchblocks that hide failures Throw exinstead ofThrow, destroying the stack trace- Ordering
Catch ex As Exceptionbefore more specific types - Leaving
On Error Resume Nextin place because "it works" - Closing a connection manually instead of using
Using - Using
FirstOrDefaultwithout testing forNothing
Practice
Search an existing VB.NET project for ArrayList, Hashtable, On Error, and empty Catch blocks, and record a count for each. Pick one On Error GoTo procedure and convert it to Try/Catch, keeping the same observable behaviour. Then pick one empty Catch block, add logging to it, run the application, and find out what it was actually suppressing — do not remove it until you know.
For the debugging drill: set a breakpoint inside a Catch block and inspect ex.InnerException and ex.StackTrace, then change Throw ex to Throw and compare the stack trace the caller sees.
You can now
- Pick the right generic collection, and recognise the legacy ones
- Read and correct all three VB.NET error-handling styles
- Say exactly what
On Error Resume Nextdoes to a program - Use
Try/Catch/FinallyandCatch ... When - Guarantee a connection or reader is released with
Using
Review questions
- What happens when a key is missing from a
Hashtablecompared with aDictionary(Of TKey, TValue)? - Why does
Throw exproduce a misleading stack trace? - What does
On Error Resume Nextdo, and why is it dangerous? - What does
Usingguarantee that a manual.Close()does not?
Next: Database connectivity