VB.NET Procedures: ByVal, ByRef, AndAlso and Try/Catch

VB.NET Sub vs Function and ByVal vs ByRef with real .NET 10 output, Select Case, loops, a NullReferenceException from And, Catch When, AndAlso timed.

Series: VB.NET lecture 2/6 (beginner). For readers who finished lecture 1 or already write simple VB, and want to write procedures and control flow that behave the way they read. After this lecture you can predict what ByVal and ByRef do with any argument, choose the right loop and branch, and write Try/Catch blocks that catch only what they should.

If you have ever "fixed" a VB bug by changing ByVal to ByRef and back without knowing why, or seen a NullReferenceException on a line that checked for Nothing, this lecture is for you. Both come from rules that look obvious and are not: what ByVal copies, and the fact that VB's And evaluates both sides. Every example below was built and run on .NET 10, with the output pasted unchanged, and the last section measures what the wrong operator costs.

Environment: .NET SDK 10.0.403, runtime 10.0.14, Visual Basic 17.13, <OptionStrict>On</OptionStrict> in the .vbproj (see lecture 1), Apple Silicon Mac (M1 Pro).

Sub vs Function, and the missing Return

A Sub does something and returns nothing. A Function returns a value with Return, or, in older code, by assigning to the function's name:

Function Square(x As Integer) As Integer
    Return x * x
End Function

Function Cube(x As Integer) As Integer
    Cube = x * x * x          ' old style, still legal
End Function

Square(4) and Cube(3) printed 16 27. The old style has a trap: a function that never reaches a Return still compiles. We built this:

Function Discount(price As Decimal) As Decimal
    If price > 100D Then
        Return price * 0.9D
    End If
End Function

Function Label(code As Integer) As String
    Select Case code
        Case 1 : Return "one"
    End Select
End Function
Program.vb(8,5): warning BC42353: Function 'Discount' doesn't return a value on all code paths. Are you missing a 'Return' statement?
Program.vb(14,5): warning BC42105: Function 'Label' doesn't return a value on all code paths. A null reference exception could occur at run time when the result is used.

Only warnings, and the build succeeds. At run time, Discount(50D) printed 0 and Label(2) Is Nothing printed True. A price of 50 becoming 0 is the kind of bug that reaches production. Treat these two warnings as errors: add <WarningsAsErrors>BC42353;BC42105</WarningsAsErrors> to the project. We checked: the same two lines then come out as error BC42353 and error BC42105 and the build fails.

ByVal and ByRef: what actually gets copied

VB passes arguments ByVal by default. The rule is short: ByVal copies the variable's contents. For a value type (Integer, Date, a Structure) that is the value. For a reference type (List, arrays, any Class) that is the reference, so both the caller and the procedure point at the same object. This full program shows every case:

Imports System
Imports System.Collections.Generic

Module Program
    Sub BumpVal(ByVal n As Integer)
        n += 1
    End Sub
    Sub BumpRef(ByRef n As Integer)
        n += 1
    End Sub
    Sub AddItemVal(ByVal list As List(Of String))
        list.Add("added")                               ' mutates the caller's object
        list = New List(Of String) From {"replaced"}    ' rebinds the local copy only
    End Sub
    Sub AddItemRef(ByRef list As List(Of String))
        list.Add("added")
        list = New List(Of String) From {"replaced"}
    End Sub
    Sub FillArray(ByVal arr As Integer())
        arr(0) = 99
    End Sub

    Sub Main()
        Dim n = 10
        BumpVal(n) : Console.WriteLine($"after BumpVal: {n}")
        BumpRef(n) : Console.WriteLine($"after BumpRef: {n}")
        BumpRef((n)) : Console.WriteLine($"after BumpRef((n)): {n}")

        Dim names As New List(Of String) From {"a"}
        AddItemVal(names) : Console.WriteLine($"ByVal list: {String.Join(",", names)}")
        names = New List(Of String) From {"a"}
        AddItemRef(names) : Console.WriteLine($"ByRef list: {String.Join(",", names)}")

        Dim arr = {1, 2, 3}
        FillArray(arr) : Console.WriteLine($"array after ByVal: {String.Join(",", arr)}")
    End Sub
End Module
after BumpVal: 10
after BumpRef: 11
after BumpRef((n)): 11
ByVal list: a,added
ByRef list: replaced
array after ByVal: 99,2,3

Three lines deserve a second look.

  • ByVal list: a,added: the procedure got its list ByVal and still changed the caller's list. "ByVal means the caller is safe" is only true for value types.
  • ByRef list: replaced: with ByRef the reassignment replaced the caller's variable, and the caller's original list (which had received "added") is gone from its view.
  • BumpRef((n)): extra parentheses turn the argument into an expression, so VB passes a temporary copy even to a ByRef parameter. n stayed 11. This is a classic surprise when someone "tidies up" parentheses.
Diagram with two rows. The caller's variable names points to a List object that ends up holding a and added. The ByVal parameter list is a copy of the reference and points to the same List, so list.Add changes the caller's object. The statement list = New List rebinds only the parameter to a new List holding replaced, which is lost when the procedure returns. With ByRef, the caller's variable itself would be rebound, and the caller would see replaced.
ByVal on a reference type copies the reference, not the object. Both variables point at the same List, so Add is visible to the caller; reassigning the parameter is not.

If a procedure must not modify a list it receives, pass IReadOnlyList(Of T) or list.AsReadOnly(); ByVal alone will not protect it.

Optional, ParamArray and overloading

Function Greet(name As String, Optional greeting As String = "Hello") As String
    Return $"{greeting}, {name}"
End Function

Function Sum(ParamArray values As Integer()) As Integer
    Dim t = 0
    For Each v In values
        t += v
    Next
    Return t
End Function

Function Describe(x As Integer) As String
    Return $"Integer {x}"
End Function
Function Describe(x As Double) As String
    Return $"Double {x}"
End Function
Function Describe(x As String) As String
    Return $"String ""{x}"""
End Function

Calls and output:

Console.WriteLine(Greet("Kim"))
Console.WriteLine(Greet("Kim", "Hi"))
Console.WriteLine(Greet(greeting:="Yo", name:="Lee"))
Console.WriteLine($"{Sum()} {Sum(1, 2, 3)} {Sum({4, 5})}")
Console.WriteLine(Describe(1))
Console.WriteLine(Describe(1.5))
Console.WriteLine(Describe("1"))
Console.WriteLine(Describe(1L))
Hello, Kim
Hi, Kim
Yo, Lee
0 6 9
Integer 1
Double 1.5
String "1"
Double 1

The last line is the trap: there is no Long overload, and Long → Integer is narrowing, so VB picks the widening Long → Double overload without a word. If you add overloads for numeric types, add one for every type callers actually pass.

The compiler errors you will meet here:

error BC30812: Optional parameters must specify a default value.
error BC30192: End of parameter list expected. Cannot define parameters after a paramarray parameter.

The first appears for Optional level As Integer with no = 0; the second for Sub Total(ParamArray xs As Integer(), label As String). ParamArray must be last.

If, Select Case and loops

Select Case in VB is more expressive than C#'s classic switch: it takes ranges, comparisons and lists in one Case.

Function Grade(score As Integer) As String
    Select Case score
        Case Is > 100, Is < 0 : Return "invalid"
        Case 90 To 100 : Return "A"
        Case 80 To 89 : Return "B"
        Case 70, 75, 79 : Return "C (exact)"
        Case Else : Return "F"
    End Select
End Function
105->invalid  95->A  85->B  75->C (exact)  72->F  -1->invalid

72 fell through to Case Else because 70, 75, 79 is a list of exact values, not a range. Cases are tested top to bottom and the first match wins; there is no fall-through to the next Case.

Loops, with Step, Continue For and Exit For:

For i = 10 To 1 Step -3
    Console.Write($"{i} ")
Next
Console.WriteLine()
For i = 1 To 10
    If i Mod 2 = 0 Then Continue For
    If i > 7 Then Exit For
    Console.Write($"{i} ")
Next
Console.WriteLine()
Dim k = 0
Do While k < 3
    k += 1
Loop
Dim j = 100
Do
    j += 1
Loop While j < 3
Console.WriteLine($"Do While: k={k}, Do...Loop While: j={j}")
Dim last = 5
For i = 1 To last
    last = 2           ' the bound was evaluated once
    Console.Write($"{i} ")
Next
10 7 4 1
1 3 5 7
Do While: k=3, Do...Loop While: j=101
1 2 3 4 5

Two details: a Do … Loop While body runs at least once (j became 101 even though 100 < 3 is false), and the For upper bound is evaluated once, so changing last inside the loop did nothing. Use Do While when the end condition changes while you loop. Exit For outside a For is error BC30096: 'Exit For' can only appear inside a 'For' statement.

Common mistake: And instead of AndAlso

The mistake. Guarding a member access with And:

Class Customer
    Public Name As String
End Class

Function FindCustomer(id As Integer) As Customer
    If id = 1 Then Return New Customer With {.Name = "Alice"}
    Return Nothing
End Function

' in Main:
Dim c = FindCustomer(2)        ' Nothing for id 2
If c IsNot Nothing AndAlso c.Name = "Alice" Then ...   ' fine
If c IsNot Nothing And c.Name = "Alice" Then ...       ' the bug

What actually happens. VB's And and Or are the old operators: they evaluate both sides, always. The check for Nothing is evaluated, then c.Name is evaluated anyway:

AndAlso: not found, no exception
And: NullReferenceException: Object reference not set to an instance of an object.

Fix. Use AndAlso and OrElse for conditions. They stop as soon as the answer is known, like && and || in C#. Keep And/Or for bit operations, where they are correct: 5 And 3 = 1, 5 Or 3 = 7, and Not 0 = -1 (VB's True is -1 when converted to an integer).

You want Use Evaluates right side?
Logical AND in an If AndAlso Only if left is True
Logical OR in an If OrElse Only if left is False
Bitwise AND/OR of integers or flags And / Or Always
Both sides have side effects you need And / Or (rare; say so in a comment) Always

Try / Catch / Finally and When filters

Function ParsePort(text As String) As Integer
    Try
        Console.WriteLine($"  try: parsing '{text}'")
        Return Integer.Parse(text)
    Catch ex As FormatException
        Console.WriteLine($"  catch FormatException")
        Return -1
    Catch ex As OverflowException When text.StartsWith("-")
        Console.WriteLine($"  catch OverflowException (negative)")
        Return 0
    Catch ex As OverflowException
        Console.WriteLine($"  catch OverflowException (positive)")
        Return 65535
    Finally
        Console.WriteLine($"  finally runs even after Return")
    End Try
End Function

For the inputs "8080", "abc", "99999999999", "-99999999999":

  try: parsing '8080'
  finally runs even after Return
ParsePort -> 8080
  try: parsing 'abc'
  catch FormatException
  finally runs even after Return
ParsePort -> -1
  try: parsing '99999999999'
  catch OverflowException (positive)
  finally runs even after Return
ParsePort -> 65535
  try: parsing '-99999999999'
  catch OverflowException (negative)
  finally runs even after Return
ParsePort -> 0

Finally ran after every Return, including the one inside Try. Catch … When is a filter: if the condition is False, that Catch is skipped and the next one is tried, without catching and rethrowing. VB has had When since its first .NET version; C# only got the equivalent (catch … when) in C# 6. Order matters: put specific exceptions and filtered catches first. For when to catch at all, see C# exception handling; the advice carries over.

In real code, Integer.TryParse is the better tool for this particular job, because exceptions are slow when they are frequent. Try/Catch belongs around things you cannot check in advance: files, network, other people's code.

Measured: And vs AndAlso, and string building in a loop

Short-circuiting is not only about safety. When the right side is expensive, And pays for it on every row. We counted matches in one million random integers (New Random(42)), with a cheap test first (x Mod 10 = 0) and a deliberately slow one second (200 iterations of hashing, standing in for a regex or a lookup):

Imports System
Imports System.Diagnostics
Imports System.Text

Module Program
    Dim calls As Long

    Function IsValid(i As Integer) As Boolean
        calls += 1
        Dim h = i
        For k = 1 To 200
            h = (h * 31 + k) And &HFFFFF
        Next
        Return h Mod 7 <> 0
    End Function

    Function CountAnd(data As Integer()) As Integer
        Dim n = 0
        For Each x In data
            If x Mod 10 = 0 And IsValid(x) Then n += 1
        Next
        Return n
    End Function

    Function CountAndAlso(data As Integer()) As Integer
        Dim n = 0
        For Each x In data
            If x Mod 10 = 0 AndAlso IsValid(x) Then n += 1
        Next
        Return n
    End Function

    Function Concat(n As Integer) As Integer
        Dim s = ""
        For i = 1 To n
            s &= "x"
        Next
        Return s.Length
    End Function

    Function Builder(n As Integer) As Integer
        Dim sb As New StringBuilder()
        For i = 1 To n
            sb.Append("x"c)
        Next
        Return sb.ToString().Length
    End Function

    Sub Time(name As String, f As Func(Of Integer))
        f()   ' warm-up
        For run = 1 To 3
            calls = 0
            Dim sw = Stopwatch.StartNew()
            Dim r = f()
            sw.Stop()
            Console.WriteLine($"{name,-14} run {run}: {sw.Elapsed.TotalMilliseconds,8:F2} ms  result={r} IsValid calls={calls}")
        Next
    End Sub

    Sub Main()
        Dim rnd As New Random(42)
        Dim data(999_999) As Integer
        For i = 0 To data.Length - 1
            data(i) = rnd.Next(1_000_000)
        Next
        Time("And", Function() CountAnd(data))
        Time("AndAlso", Function() CountAndAlso(data))
        For Each n In {10_000, 50_000, 100_000}
            Time($"&= {n}", Function() Concat(n))
            Time($"SB {n}", Function() Builder(n))
        Next
    End Sub
End Module

Release build, Apple Silicon Mac (M1 Pro), three timed runs after one warm-up call:

Variant Run 1 Run 2 Run 3 IsValid calls Result
And 315.97 ms 313.77 ms 312.89 ms 1,000,000 86,118
AndAlso 33.41 ms 33.04 ms 33.23 ms 100,381 86,118
Bar chart. With And, the expensive check ran 1,000,000 times and the loop took 312.9 ms. With AndAlso, it ran 100,381 times and the loop took 33.2 ms. Both counted the same 86,118 matches.
Counting matches in 1,000,000 seeded random ints where a cheap test comes first and an expensive one second. Release build, .NET 10, Apple Silicon Mac (M1 Pro), third timed run after warm-up.

Same answer, about 9.4 times faster, because the slow check ran only for the 100,381 numbers divisible by 10. The speed-up is roughly the ratio of calls, which is the honest way to predict it for your own data.

The same program also timed building a string of n characters:

n s &= "x" (run 3) StringBuilder (run 3)
10,000 9.26 ms 0.03 ms
50,000 114.55 ms 0.08 ms
100,000 511.12 ms 0.15 ms

Five times the characters cost about twelve times the time with &=, and doubling again cost 4.5 times more: every &= copies the whole string, so the cost grows with the square of the length. StringBuilder stayed under a fifth of a millisecond. For a handful of concatenations & is fine; inside a loop, use StringBuilder or String.Join.

Honest limits / version notes

  • All code was built and run on .NET SDK 10.0.403, runtime 10.0.14, VB 17.13, on macOS, with Option Strict On. We did not test Visual Studio on Windows or .NET Framework. AndAlso, OrElse and Catch … When have existed since VB.NET 2002 and Continue For since VB 2005, but we only verified them on .NET 10.
  • The parenthesised-argument rule (BumpRef((n))) applies to any expression, including a property passed ByRef; we tested only the local-variable case.
  • The IsValid work is synthetic. Your speed-up depends on how often the left side is False and how slow the right side is.
  • Compiler messages above are the English versions (DOTNET_CLI_UI_LANGUAGE=en); the BC numbers are the same in every UI language.

Previous: VB.NET lecture 1/6: Option Strict, types and Nothing. Next: VB.NET lecture 3/6: classes, inheritance and interfaces. Related: C# methods, C# if/else and switch, C# loops.

FAQ

If I pass a List ByVal in VB.NET, can the procedure change it?

Yes. ByVal copies the reference, not the object. In our run, a procedure that received a List ByVal added an item and the caller saw it (a,added). Only reassigning the parameter to a new List stayed invisible to the caller.

What is the difference between And and AndAlso in VB.NET?

And always evaluates both sides; AndAlso stops when the left side is False. `c IsNot Nothing And c.Name = "Alice"` threw NullReferenceException when c was Nothing; the AndAlso version did not. With an expensive second test, AndAlso was 33 ms versus 313 ms for And.

What happens if a VB.NET Function forgets to Return?

It compiles with warning BC42353 (or BC42105 for reference types) and returns the type's default value. Our Discount(50D) silently returned 0.

  • #VB.NET
  • #.NET 10
  • #ByRef
  • #AndAlso
  • #beginner
  • #control flow

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑