Skip to main content
Published / updated

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

  • ArrayList and Hashtable versus List(Of T) and Dictionary(Of TKey, TValue)
  • Why untyped collections are a defect under Option Strict Off
  • Try, Catch, Finally, Throw
  • Catch ... When filters
  • Exception types worth catching, and the ones to leave alone
  • On Error GoTo and On Error Resume Next
  • Using and 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)
LegacyGeneric replacementNote
ArrayListList(Of T)Direct replacement
HashtableDictionary(Of TKey, TValue)Missing key: Hashtable returns Nothing, Dictionary throws
SortedListSortedList(Of TKey, TValue)
Queue / StackQueue(Of T) / Stack(Of T)
CollectionList(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 Error and Try/Catch cannot be mixed in the same procedure — the compiler rejects it
  • Convert one procedure at a time, and test it
  • Treat On Error Resume Next as 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

MessageCauseFix
System.InvalidCastException reading from an ArrayListLegacy untyped collection holding mixed typesMove 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 wronglyOn Error Resume NextReplace with Try/Catch, one routine at a time
Stack trace points at the rethrow, not the failureThrow ex instead of bare ThrowUse Throw
System.InvalidOperationException: Collection was modifiedRemoved inside For EachLoop backwards with For
Connection stays open after an errorNo Using blockWrap 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 ArrayList and Hashtable in code that is otherwise modern
  • Swapping Hashtable for Dictionary without handling the missing-key throw
  • Swapping VB6 Collection for List(Of T) and inheriting an off-by-one from one-based indexing
  • Empty Catch blocks that hide failures
  • Throw ex instead of Throw, destroying the stack trace
  • Ordering Catch ex As Exception before more specific types
  • Leaving On Error Resume Next in place because "it works"
  • Closing a connection manually instead of using Using
  • Using FirstOrDefault without testing for Nothing

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 Next does to a program
  • Use Try/Catch/Finally and Catch ... When
  • Guarantee a connection or reader is released with Using

Review questions

  1. What happens when a key is missing from a Hashtable compared with a Dictionary(Of TKey, TValue)?
  2. Why does Throw ex produce a misleading stack trace?
  3. What does On Error Resume Next do, and why is it dangerous?
  4. What does Using guarantee that a manual .Close() does not?

Next: Database connectivity