Best Practices and Common Pitfalls

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 program that starts with Option Strict On, was built with warnings treated as errors (except where the text says otherwise) and was run in the .NET 10 SDK container (Linux, arm64); the output shown is real output. The examples use the shelf library-catalogue types where a domain object helps. The root namespace of the scratch projects is ShelfApp.

Most Visual Basic surprises come from three sources: defaults that are lenient for historical reasons (the SDK turns Option Strict off), operators and runtime functions that keep Visual Basic 6 behaviour (/ returns a Double, Mid is 1-based, And does not short-circuit) and a few places where the language differs from C# in a way that compiles without a warning (case-insensitive names, loop locals that are not reset, loop bounds evaluated once). This page collects the rules that prevent them, shows each failure with real output, and ends each rule with the Learn page that states it. The detailed treatment of each feature is on its own page; this one is the checklist you read before a code review.

The rules at a glance

Area Rule

Project

Set <OptionStrict>On</OptionStrict> in the project (or in Directory.Build.props) and keep Option Explicit On, Option Infer On. Treat warnings as errors in CI.

Conversions

No implicit narrowing. Use CInt, CType, DirectCast, TryCast and the TryParse methods on purpose.

Strings

Join text with & or interpolation ($"..."), never +. Compare with String.Equals and an explicit StringComparison when case or culture matters.

Logic

Use AndAlso and OrElse in conditions. Use Is Nothing and IsNot Nothing for references. Use If(a, b) where C# has ??.

Numbers

/ is floating-point, \ is integer division, ^ always returns Double, CInt and Math.Round round halves to even, integer arithmetic is overflow-checked.

Arrays and indexes

Dim items(3) declares four elements. Runtime-library functions (Mid, InStr) are 1-based; framework methods (Substring, IndexOf) are 0-based.

Loops and scope

Initialise every local declared inside a loop. Remember that For i = a To b evaluates b once. Never change a collection inside For Each.

Culture

Pass a CultureInfo (usually CultureInfo.InvariantCulture) when parsing or formatting dates and numbers that are stored or exchanged.

Resources

Wrap every IDisposable in Using. Do not rely on the garbage collector to flush or close anything.

Errors

Use Try/Catch. On Error belongs only to code ported from Visual Basic 6.

Async

Return Task from asynchronous methods. Async Sub is for event handlers only.

Libraries

Do not use default form instances (Form1.Show()) or My.Forms in reusable code. Keep late binding (Object plus CallByName) in one small, labelled place.

Names

PascalCase for types and members, camelCase for locals and parameters, no Hungarian prefixes, and no two names in the same scope that differ only by case.

The gotcha table

Each row names the pitfall, shows the wrong form, the right form, and where the page demonstrates it with real output. The last column is the Learn page that states the rule. Rows whose "wrong" form is a construct this documentation never shows in code (string concatenation with +, the old IIf function, On Error, Option Strict Off) describe it in words.

Pitfall Wrong Right Shown in Learn

Implicit conversions compile

A project without OptionStrict: the SDK default is Off, so a Double silently becomes an Integer.

<OptionStrict>On</OptionStrict>; the same lines then fail with BC30512.

Make the compiler say no

Option Strict Statement

+ is not concatenation

Joining a String and a number with +. With Option Strict On it is BC30512 (String to Double and back); with it off, it adds or throws at run time.

"Copies: " & copies or $"Copies: {copies}".

Make the compiler say no

Concatenation Operators

And/Or do not short-circuit

If title Is Nothing Or title.Length = 0 throws NullReferenceException when title is Nothing.

If title Is Nothing OrElse title.Length = 0

Conditions that stop early

AndAlso Operator

= Nothing on strings

empty = Nothing is True for the empty string.

empty Is Nothing for references, String.IsNullOrEmpty(empty) for text.

Conditions that stop early

Is Operator

Nothing is not always null

Dim count As Integer = Nothing is 0, Dim flag As Boolean = Nothing is False.

Use Integer? when "no value" must be distinguishable.

Conditions that stop early

Nothing

Declarations give the upper bound

Dim items(3) As Integer is assumed to hold three elements; it holds four.

Dim items(count - 1) As Integer, or New Integer(count - 1) {}; use .Length.

division, rounding and indexes

Arrays

/ is not integer division

7 / 2 is 3.5, a Double, even for two Integer operands.

7 \ 2 is 3. 7 / 0 is Infinity; 7 \ 0 throws DivideByZeroException.

division, rounding and indexes

Arithmetic Operators

^ returns Double

Dim p As Integer = 2 ^ 10 fails with BC30512.

CInt(2 ^ 10); use BigInteger.Pow for big integers.

division, rounding and indexes

Arithmetic Operators

Conversions round halves to even

CInt(2.5) is 2 and CInt(3.5) is 4; Math.Round(2.5) is 2.

Math.Round(x, MidpointRounding.AwayFromZero) when you mean schoolbook rounding.

division, rounding and indexes

Math.Round

Integer arithmetic is checked

Integer.MaxValue + 1 is assumed to wrap, as in C#; it throws OverflowException.

Keep the check (it is a feature), or use Long; RemoveIntegerChecks removes it project-wide and changes this output.

division, rounding and indexes

-removeintchecks

1-based runtime functions

Mid(text, 6, 5) after reading Substring(6, 5). InStr returns 0 for "not found".

Mid(text, 7, 5) equals text.Substring(6, 5). IndexOf returns -1.

division, rounding and indexes

Strings.Mid

Case-insensitive names

Name = name in a constructor assigns the parameter to itself; the property stays Nothing.

Me.Name = name

scope and names

Declared Element Names

Loop locals are not reset

Dim counter As Integer inside a loop keeps its value from the previous pass.

Dim counter As Integer = 0

scope and names

Dim Statement

For bound evaluated once

For i = 0 To titles.Count - 1 with titles.RemoveAt(i) inside throws ArgumentOutOfRangeException.

titles.RemoveAll(predicate), or count down from the end.

scope and names

For…​Next Statement

Changing a collection in For Each

titles.Add(x) inside For Each t In titles throws InvalidOperationException.

Iterate over a copy (titles.ToList()), or collect changes and apply them afterwards.

scope and names

For Each…​Next Statement

Culture-dependent parsing

Date.Parse("03/04/2026") is 4 March in en-US and 3 April in en-GB; 1.234,50 parses only in de-DE.

Date.ParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture); pass a culture to every Parse.

Dates and numbers need a culture

Parsing date and time strings

Date literals ignore the culture

#3/4/2026# is 4 March everywhere (month/day/year).

Write New Date(2026, 4, 3) or an ISO string with ParseExact.

Dates and numbers need a culture

Data types

Missing Using

A StreamWriter that is not disposed leaves its buffer unwritten: 0 bytes on disk.

Using writer As New StreamWriter(path)

Resources need Using

Using Statement

Async Sub

An exception thrown after an Await cannot be caught by the caller and ends the process.

Async Function ... As Task; Async Sub only for event handlers.

not Async Sub

Async/Await best practices

?? does not exist

title ?? "untitled" is a syntax error (BC36637).

If(title, "untitled")

Habits to leave behind

If Operator

On Error in new code

On Error Resume Next hides failures.

Try/Catch/Finally and Using; see Structured Exception Handling.

Habits to leave behind

Try…​Catch…​Finally Statement

Default form instances

Form1.Show() in a library depends on My.Forms and the Application Framework.

Dim form As New Form1(); see Windows Forms Applications.

Habits to leave behind

Early and late binding

Late binding everywhere

Object variables and CallByName spread through the code base.

Typed variables and interfaces; isolate unavoidable late binding in one small helper (see Early and Late Binding).

Habits to leave behind

Early and late binding

Make the compiler say no

The SDK defaults are OptionExplicit On, OptionInfer On, OptionCompare Binary and OptionStrict Off, so a new project accepts conversions that lose information or fail at run time. Turn the check on once, for the whole project, so that a file cannot forget it:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

The files in this documentation also start with Option Strict On, which is harmless next to the property and keeps a copied file safe in another project. The following program has five implicit conversions, one per line; each is a different way the old defaults hide a bug.

Option Strict On

Module Program
    Sub Main()
        Dim power As Integer = 2 ^ 10
        Dim average As Integer = 7 / 2
        Dim text As String = 42
        Dim flag As Boolean = "True"
        Dim d As Date = "2026-04-03"
        Console.WriteLine(power & average & text & flag & d)
    End Sub
End Module

The compiler rejects every line (warnings as errors turned off only so that the errors are the whole output):

Program.vb(5,32): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(6,34): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(7,30): error BC30512: Option Strict On disallows implicit conversions from 'Integer' to 'String'.
Program.vb(8,31): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Boolean'.
Program.vb(9,25): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Date'.

The five cases are ^ returning Double, / returning Double, a number stored as text, text stored as a Boolean and a string stored as a Date. With the setting off, the first two truncate or round without a word, the third and fourth work only when the text happens to parse, and the fifth depends on the culture of the machine. Two further cases are worth knowing by their messages. Joining a String and an Integer with + reports BC30512 twice on the same line, from String to Double and from Double to String, and the null-coalescing operator of C# is a syntax error:

Program.vb(6,31): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Double'.
Program.vb(6,31): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'String'.
Program.vb(8,27): error BC36637: The '?' character cannot be used here.

(These lines came from two small failing programs, not shown. The first one has the + statement; the second has title ?? "untitled" and, because a syntax error hides semantic errors, was compiled separately.) The replacement for ?? is the two-argument If.

For the three other Option statements and the project-level properties see Option Statements. Warnings turn into build failures with <TreatWarningsAsErrors>; the analyzers that catch more of these mistakes are on XML Documentation, Analyzers and Code Style.

Conditions that stop early

And, Or and Xor are bitwise operators that also work on Booleans, so they always evaluate both operands. AndAlso and OrElse are the short-circuiting logical operators and are the ones to use in conditions. Visual Basic 6 had no short-circuiting operators, which is why the old habit survives in older books.

Option Strict On

Module Program
    Private Function Check(label As String, result As Boolean) As Boolean
        Console.WriteLine($"  evaluated {label}")
        Return result
    End Function

    Sub Main()
        Console.WriteLine("And evaluates both operands:")
        Dim r1 = Check("A", False) And Check("B", True)
        Console.WriteLine($"  result {r1}")
        Console.WriteLine("AndAlso stops at the first False:")
        Dim r2 = Check("A", False) AndAlso Check("B", True)
        Console.WriteLine($"  result {r2}")
        Console.WriteLine("OrElse stops at the first True:")
        Dim r3 = Check("A", True) OrElse Check("B", False)
        Console.WriteLine($"  result {r3}")

        Dim title As String = Nothing
        Try
            If title Is Nothing Or title.Length = 0 Then
                Console.WriteLine("Or: empty")
            End If
        Catch ex As NullReferenceException
            Console.WriteLine($"Or with a Nothing string -> {ex.GetType().Name}")
        End Try
        If title Is Nothing OrElse title.Length = 0 Then
            Console.WriteLine("OrElse: empty")
        End If

        ' Nothing and the empty string compare equal with =.
        Dim empty As String = ""
        Console.WriteLine($"empty = Nothing   : {empty = Nothing}")
        Console.WriteLine($"empty Is Nothing  : {empty Is Nothing}")
        Console.WriteLine($"String.IsNullOrEmpty(empty): {String.IsNullOrEmpty(empty)}")

        ' Nothing for a value type is its default value.
        Dim count As Integer = Nothing
        Dim flag As Boolean = Nothing
        Dim stamp As Date = Nothing
        Console.WriteLine($"Integer = Nothing -> {count}, Boolean -> {flag}, Date -> {stamp:yyyy-MM-dd}")

        ' String comparison is case-sensitive and binary by default.
        Dim left = "Shelf"
        Dim right = "SHELF"
        Console.WriteLine($"left = right: {left = right}")
        Console.WriteLine($"String.Equals OrdinalIgnoreCase: {String.Equals(left, right, StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"String.Compare(left, right, True) = {String.Compare(left, right, True, Globalization.CultureInfo.InvariantCulture)}")

        ' Concatenation: & converts both sides to String.
        Dim copies As Integer = 3
        Console.WriteLine("Copies: " & copies & ", title: [" & title & "]")
        Console.WriteLine($"Copies: {copies}, title: [{title}]")
    End Sub
End Module
And evaluates both operands:
  evaluated A
  evaluated B
  result False
AndAlso stops at the first False:
  evaluated A
  result False
OrElse stops at the first True:
  evaluated A
  result True
Or with a Nothing string -> NullReferenceException
OrElse: empty
empty = Nothing   : True
empty Is Nothing  : False
String.IsNullOrEmpty(empty): True
Integer = Nothing -> 0, Boolean -> False, Date -> 0001-01-01
left = right: False
String.Equals OrdinalIgnoreCase: True
String.Compare(left, right, True) = 0
Copies: 3, title: []
Copies: 3, title: []

Five things to read from the output:

  • And printed both "evaluated A" and "evaluated B" even though the first operand was already False. When the second operand is a null check or has a side effect, that is a bug; with Or and a Nothing string it was a NullReferenceException.

  • empty = Nothing is True for the empty string, because the comparison operator treats Nothing as the empty string for String operands. Is Nothing asks the real question, whether the reference is null. For "no text" use String.IsNullOrEmpty or String.IsNullOrWhiteSpace.

  • Nothing assigned to a value type is its default value, so an Integer set to Nothing is 0 and a Date is 1 January 0001. Use Integer? and HasValue when "absent" has to be different from zero (see Nothing and Nullable Value Types).

  • String comparison with = is case-sensitive and binary under Option Compare Binary. String.Equals with StringComparison.OrdinalIgnoreCase is the explicit way to ignore case, and it does not depend on the Option Compare setting of the file.

  • & converted the Integer and the Nothing string to text; Nothing became an empty string and the interpolated string did the same. & is the concatenation operator; + is addition that happens to work on two strings.

Source: Is Operator

The full operator tables are on Operators and Expressions.

Arrays, division, rounding and indexes

Option Strict On

Module Program
    Sub Main()
        ' Declarations give the upper bound, not the length.
        Dim items(3) As Integer
        Console.WriteLine($"Dim items(3): Length = {items.Length}, UBound = {UBound(items)}, LBound = {LBound(items)}")
        Dim emptyArray(-1) As Integer
        Console.WriteLine($"Dim emptyArray(-1): Length = {emptyArray.Length}")
        Dim four = New Integer(3) {}
        Console.WriteLine($"New Integer(3) {{}}: Length = {four.Length}")

        ' Division operators.
        Dim a As Integer = 7
        Dim b As Integer = 2
        Console.WriteLine($"7 / 2  = {a / b} ({(a / b).GetType().Name})")
        Console.WriteLine($"7 \ 2  = {a \ b} ({(a \ b).GetType().Name})")
        Console.WriteLine($"-7 \ 2 = {-a \ b}, -7 Mod 2 = {-a Mod b}")
        Dim zero As Integer = 0
        Console.WriteLine($"7 / 0  = {a / zero}")
        Try
            Console.WriteLine(a \ zero)
        Catch ex As DivideByZeroException
            Console.WriteLine($"7 \ 0  -> {ex.GetType().Name}")
        End Try

        ' Exponentiation always yields Double.
        Dim power = 2 ^ 10
        Console.WriteLine($"2 ^ 10 is {power} ({power.GetType().Name}); CInt gives {CInt(power)}")

        ' Banker's rounding in conversions and Math.Round.
        Dim values As Double() = {0.5, 1.5, 2.5, 3.5, -2.5}
        For Each v In values
            Console.WriteLine($"{v,4}: CInt = {CInt(v),2}, Math.Round = {Math.Round(v),2}, AwayFromZero = {Math.Round(v, MidpointRounding.AwayFromZero),2}, Int = {Int(v),2}, Fix = {Fix(v),2}")
        Next

        ' Integer arithmetic is checked in Visual Basic.
        Dim big As Integer = Integer.MaxValue
        Try
            Dim sum = big + 1
            Console.WriteLine(sum)
        Catch ex As OverflowException
            Console.WriteLine($"Integer.MaxValue + 1 -> {ex.GetType().Name}: {ex.Message}")
        End Try

        ' Runtime functions are 1-based, framework methods 0-based.
        Dim text = "Hello World!"
        Console.WriteLine($"Mid(text, 7, 5)      = '{Mid(text, 7, 5)}'")
        Console.WriteLine($"text.Substring(6, 5) = '{text.Substring(6, 5)}'")
        Console.WriteLine($"InStr(text, ""World"")  = {InStr(text, "World")}")
        Console.WriteLine($"text.IndexOf(""World"") = {text.IndexOf("World", StringComparison.Ordinal)}")
        Console.WriteLine($"InStr(text, ""xyz"")    = {InStr(text, "xyz")}")
        Console.WriteLine($"text.IndexOf(""xyz"")   = {text.IndexOf("xyz", StringComparison.Ordinal)}")
    End Sub
End Module
Dim items(3): Length = 4, UBound = 3, LBound = 0
Dim emptyArray(-1): Length = 0
New Integer(3) {}: Length = 4
7 / 2  = 3.5 (Double)
7 \ 2  = 3 (Int32)
-7 \ 2 = -3, -7 Mod 2 = -1
7 / 0  = Infinity
7 \ 0  -> DivideByZeroException
2 ^ 10 is 1024 (Double); CInt gives 1024
 0.5: CInt =  0, Math.Round =  0, AwayFromZero =  1, Int =  0, Fix =  0
 1.5: CInt =  2, Math.Round =  2, AwayFromZero =  2, Int =  1, Fix =  1
 2.5: CInt =  2, Math.Round =  2, AwayFromZero =  3, Int =  2, Fix =  2
 3.5: CInt =  4, Math.Round =  4, AwayFromZero =  4, Int =  3, Fix =  3
-2.5: CInt = -2, Math.Round = -2, AwayFromZero = -3, Int = -3, Fix = -2
Integer.MaxValue + 1 -> OverflowException: Arithmetic operation resulted in an overflow.
Mid(text, 7, 5)      = 'World'
text.Substring(6, 5) = 'World'
InStr(text, "World")  = 7
text.IndexOf("World") = 6
InStr(text, "xyz")    = 0
text.IndexOf("xyz")   = -1
  • Array declarations give the upper bound. Dim items(3) has indexes 0 to 3, so Length is 4 and UBound is 3. Dim emptyArray(-1) is the empty array. The New Integer(3) {} form uses the same rule. Compute the bound as count - 1, and read Length rather than UBound + 1. See Arrays.

  • Division. / converted two Integer operands to Double, so 7 / 0 is Infinity rather than an exception, and 7 \ 0 throws DivideByZeroException. \ truncates toward zero (-7 \ 2 is -3) and Mod takes the sign of the left operand (-7 Mod 2 is -1).

  • Exponentiation. ^ always returns Double; CInt(power) converts back and throws OverflowException if the result does not fit.

  • Rounding. CInt and Math.Round use banker’s rounding, so 2.5 becomes 2 and 3.5 becomes 4. Int rounds toward negative infinity (Int(-2.5) is -3) while Fix rounds toward zero (-2). Pass MidpointRounding.AwayFromZero when the specification says "round half up". See Numbers and Math.

  • Overflow is checked. Integer.MaxValue + 1 threw OverflowException; C# wraps unless it is in a checked context. The check can be removed for a whole assembly with the RemoveIntegerChecks project property, which is a project-wide change in behaviour and is never a fix for a logic error. A constant expression that overflows is a compile error: Dim big As Integer = Integer.MaxValue + 1 fails with BC30439, "Constant expression not representable in type 'Integer'"; the program above hides the constant in a variable.

  • Runtime functions are 1-based. Mid(text, 7, 5) and text.Substring(6, 5) return the same five characters. InStr returns 0 for "not found" and a 1-based position otherwise; IndexOf returns -1 and a 0-based position. Mixing the two families without converting is the commonest off-by-one in migrated code.

Source: Arrays

Source: Strings.Mid

Loops, scope and names

Option Strict On

Public Class Shelf
    Public Property Name As String
    Public Property Capacity As Integer

    Public Sub New(name As String, capacity As Integer)
        Name = name
        Me.Capacity = capacity
    End Sub
End Class

Public Class ShelfFixed
    Public Property Name As String

    Public Sub New(name As String)
        Me.Name = name
    End Sub
End Class

Module Program
    Sub Main()
        Console.WriteLine("A Dim inside a loop is not reset on each pass:")
        For pass = 1 To 3
            Dim counter As Integer
            counter += 1
            Console.WriteLine($"  pass {pass}: counter = {counter}")
        Next
        Console.WriteLine("Initialising it resets it every time:")
        For pass = 1 To 3
            Dim counter As Integer = 0
            counter += 1
            Console.WriteLine($"  pass {pass}: counter = {counter}")
        Next

        Console.WriteLine("The To bound is evaluated once:")
        Dim titles As New List(Of String) From {"A", "B", "C", "D"}
        Try
            For i = 0 To titles.Count - 1
                If titles(i) = "B" OrElse titles(i) = "C" Then titles.RemoveAt(i)
            Next
        Catch ex As ArgumentOutOfRangeException
            Console.WriteLine($"  RemoveAt inside For -> {ex.GetType().Name}")
        End Try
        Dim copy As New List(Of String) From {"A", "B", "C", "D"}
        copy.RemoveAll(Function(t) t = "B" OrElse t = "C")
        Console.WriteLine($"  RemoveAll -> {String.Join(",", copy)}")

        Console.WriteLine("Changing a collection inside For Each:")
        Try
            For Each t In titles
                If t = "A" Then titles.Add("E")
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine($"  {ex.GetType().Name}: {ex.Message}")
        End Try

        Console.WriteLine("Identifiers are case-insensitive:")
        Dim s1 As New Shelf("Fiction", 40)
        Dim s2 As New ShelfFixed("Fiction")
        Console.WriteLine($"  Name = name in the constructor -> Name is [{s1.Name}], Capacity {s1.Capacity}")
        Console.WriteLine($"  Me.Name = name                 -> Name is [{s2.Name}]")
    End Sub
End Module
A Dim inside a loop is not reset on each pass:
  pass 1: counter = 1
  pass 2: counter = 2
  pass 3: counter = 3
Initialising it resets it every time:
  pass 1: counter = 1
  pass 2: counter = 1
  pass 3: counter = 1
The To bound is evaluated once:
  RemoveAt inside For -> ArgumentOutOfRangeException
  RemoveAll -> A,D
Changing a collection inside For Each:
  InvalidOperationException: Collection was modified; enumeration operation may not execute.
Identifiers are case-insensitive:
  Name = name in the constructor -> Name is [], Capacity 40
  Me.Name = name                 -> Name is [Fiction]

Five rules come from this program:

  • A local declared with Dim inside a loop body has procedure scope in practice: it is not recreated on each pass, so without an initialiser it keeps the value of the previous pass (counter printed 1, 2, 3). Write the initialiser every time, or declare the variable where its lifetime is clear. Reference-type locals are the same: they are Nothing only on the first pass.

  • For i = 0 To titles.Count - 1 evaluates titles.Count - 1 once. Removing items inside the loop leaves the counter pointing past the end, which threw ArgumentOutOfRangeException. RemoveAll with a predicate is the right tool; so is iterating from the last index to the first.

  • For Each over a List(Of T) detects modification and throws InvalidOperationException with the message printed above.

  • Identifiers are case-insensitive. In the constructor Name = name the parameter name hides the property Name, so the statement assigns the parameter to itself and the property stays Nothing. The compiler reports nothing; Capacity worked only because the code wrote Me.Capacity. Always qualify members with Me. when a parameter has the same name apart from case, or name parameters differently.

  • The same case-insensitivity makes a local called text clash with System.Text, and a module called Before with a variable called before (BC30980). The extra rule is "no two names in a scope that differ only by case".

Dates and numbers need a culture

Date.Parse(text) and Decimal.Parse(text) use the culture of the current thread, which is the culture of the machine unless the program sets it. Text that came from a file, a socket or a database should use an explicit culture or an exact format.

Option Strict On
Imports System.Globalization

Module Program
    Sub Main()
        Dim text = "03/04/2026"
        Dim us = New CultureInfo("en-US")
        Dim gb = New CultureInfo("en-GB")
        Dim fromUs = Date.Parse(text, us)
        Dim fromGb = Date.Parse(text, gb)
        Console.WriteLine($"Date.Parse(""{text}"", en-US) = {fromUs:yyyy-MM-dd}")
        Console.WriteLine($"Date.Parse(""{text}"", en-GB) = {fromGb:yyyy-MM-dd}")

        Dim iso = Date.ParseExact("2026-04-03", "yyyy-MM-dd", CultureInfo.InvariantCulture)
        Console.WriteLine($"ParseExact yyyy-MM-dd         = {iso:yyyy-MM-dd}")

        Dim literal = #3/4/2026#
        Console.WriteLine($"#3/4/2026# literal            = {literal:yyyy-MM-dd} (always month/day/year)")

        Dim d = New Date(2026, 4, 3)
        Console.WriteLine($"ToString(en-US) = {d.ToString("d", us)}")
        Console.WriteLine($"ToString(en-GB) = {d.ToString("d", gb)}")
        Console.WriteLine($"ToString(""O"", Invariant) = {d.ToString("O", CultureInfo.InvariantCulture)}")

        Dim price = 1234.5D
        Console.WriteLine($"{price.ToString("N2", us)} / {price.ToString("N2", New CultureInfo("de-DE"))} / {price.ToString("N2", CultureInfo.InvariantCulture)}")
        Dim parsed As Decimal
        Dim ok = Decimal.TryParse("1.234,50", NumberStyles.Number, New CultureInfo("de-DE"), parsed)
        Console.WriteLine($"Decimal.TryParse(""1.234,50"", de-DE) = {ok}, {parsed.ToString(CultureInfo.InvariantCulture)}")
        Dim wrong As Decimal
        Dim ok2 = Decimal.TryParse("1.234,50", NumberStyles.Number, CultureInfo.InvariantCulture, wrong)
        Console.WriteLine($"Decimal.TryParse(""1.234,50"", Invariant) = {ok2}")
    End Sub
End Module
Date.Parse("03/04/2026", en-US) = 2026-03-04
Date.Parse("03/04/2026", en-GB) = 2026-04-03
ParseExact yyyy-MM-dd         = 2026-04-03
#3/4/2026# literal            = 2026-03-04 (always month/day/year)
ToString(en-US) = 4/3/2026
ToString(en-GB) = 03/04/2026
ToString("O", Invariant) = 2026-04-03T00:00:00.0000000
1,234.50 / 1.234,50 / 1,234.50
Decimal.TryParse("1.234,50", de-DE) = True, 1234.50
Decimal.TryParse("1.234,50", Invariant) = False
  • The same text 03/04/2026 is 4 March in en-US and 3 April in en-GB. A program tested on one machine silently misreads dates on another.

  • Date.ParseExact with yyyy-MM-dd and CultureInfo.InvariantCulture does not depend on the machine, and the round-trip format O does not either.

  • A date literal in source code is always month/day/year, whatever the culture: #3/4/2026# is 4 March.

  • N2 formatted the same number as 1,234.50, 1.234,50 and 1,234.50 in three cultures, and the German text parsed only with the German culture. Use CultureInfo.InvariantCulture for stored data and the user’s culture only for display.

The culture names depend on the operating system’s globalization data (ICU on Linux). The program above ran on the Ubuntu-based SDK container.

Resources need Using

Option Strict On
Imports System.IO

Module Program
    Sub Main()
        Dim path1 = Path.Combine(Path.GetTempPath(), "shelf-no-using.txt")
        Dim path2 = Path.Combine(Path.GetTempPath(), "shelf-using.txt")

        Dim leaky As New StreamWriter(path1)
        leaky.WriteLine("Dune")
        ' No Dispose or Flush: the data is still in the writer's buffer.
        Dim sizeLeaky = New FileInfo(path1).Length

        Using writer As New StreamWriter(path2)
            writer.WriteLine("Dune")
        End Using
        Dim sizeUsing = New FileInfo(path2).Length

        Console.WriteLine($"without Using: {sizeLeaky} bytes on disk")
        Console.WriteLine($"with Using   : {sizeUsing} bytes on disk")

        leaky.Dispose()
        File.Delete(path1)
        File.Delete(path2)
    End Sub
End Module
without Using: 0 bytes on disk
with Using   : 5 bytes on disk

The first writer was never disposed or flushed, so its buffer never reached the file. The garbage collector does not flush buffers for you, and a finalizer is not guaranteed to run before the process ends. Using calls Dispose even if an exception is thrown, and it works with any type that implements IDisposable. Visual Basic has no using var declaration and no Await Using; see Object Lifetime and Disposal.

Source: Using Statement

Async Function, not Async Sub

Option Strict On
Imports System.Threading.Tasks

Module Program
    Async Function LoadAsync() As Task
        Await Task.Delay(10)
        Throw New InvalidOperationException("catalogue unavailable")
    End Function

    Async Sub LoadFireAndForget()
        Await Task.Delay(10)
        Throw New InvalidOperationException("catalogue unavailable")
    End Sub

    Sub Main()
        Try
            LoadAsync().GetAwaiter().GetResult()
        Catch ex As InvalidOperationException
            Console.WriteLine($"Async Function: caught by the caller -> {ex.Message}")
        End Try

                AddHandler AppDomain.CurrentDomain.UnhandledException,
            Sub(sender, e)
                Console.WriteLine($"Async Sub: nobody could catch it -> UnhandledException: {DirectCast(e.ExceptionObject, Exception).Message}")
                Console.Out.Flush()
                Environment.Exit(3)
            End Sub

        Try
            LoadFireAndForget()
            Console.WriteLine("Async Sub: the Try block already finished")
        Catch ex As InvalidOperationException
            Console.WriteLine("never printed")
        End Try
        System.Threading.Thread.Sleep(2000)
    End Sub
End Module
Async Function: caught by the caller -> catalogue unavailable
Async Sub: the Try block already finished
Async Sub: nobody could catch it -> UnhandledException: catalogue unavailable

The process exit code was 3, set by the handler, because an exception thrown after an Await in an Async Sub is raised on the thread pool (or on the captured synchronization context) where nothing can catch it. The Try block around the call had finished before the exception happened. An Async Function that returns Task stores the exception in the task, and the caller sees it when it awaits. Use Async Sub only for event handlers, where the signature is fixed. Visual Basic also has no Async Main, which is why Sub Main calls GetAwaiter().GetResult(). See Async and Await.

Source: Async

Habits to leave behind

Habit Replacement

On Error GoTo and Resume Next

Try/Catch/Finally. The unstructured statements remain only so that ported Visual Basic 6 code still compiles; see Unstructured Error Handling.

The conditional function from Visual Basic 6 (IIf)

The If(condition, a, b) operator, which evaluates only the branch that is used. The function evaluates both arguments before calling.

Default form instances (Form1.Show())

Create the form: Dim form As New Form1(). Default instances come from the Application Framework and My.Forms, which exist only in Windows Forms projects with the framework enabled and make a library depend on the application that hosts it.

Object variables and CallByName everywhere

Typed variables, interfaces and generics. When late binding is unavoidable (COM, scripting hosts), put it in one small class and document it. It is the only place Option Strict Off is ever excused; see Early and Late Binding.

Hungarian prefixes (strName, intCount, clsBook)

The .NET naming guidelines: no type prefixes, PascalCase members, camelCase locals, I prefix for interfaces. The coding conventions page on Learn says the same.

Dim a, b As Integer copied from Visual Basic 6 expectations

Modern Visual Basic gives both variables the type Integer (in Visual Basic 6 only b was). Still prefer one declaration per line.

Return versus assigning to the function name

Return value ends the function immediately; assigning to the function name does not. Use Return.

Handles plus default instances plus Public fields in forms

Private fields, events and properties; keep the designer file generated.

The rules applied

The last example reads catalogue lines and applies the rules together: Option Strict On, culture-aware parsing with NumberStyles.None and InvariantCulture, TryParse instead of exceptions for expected bad input, Is Nothing for the null check, AndAlso and OrElse, Using over the reader and & for the line concatenation. It needs Domain.vb of the shelf next to it (the Isbn structure validates the check digit; the full listing is on Design Patterns in Visual Basic).

Option Strict On
Imports System.Globalization
Imports System.IO

Module Program
    Private Function ParseLine(line As String) As Book
        Dim parts = line.Split(";"c)
        If parts.Length <> 3 Then Return Nothing

        Dim isbn As Isbn = Nothing
        Dim year As Integer
        If Not Isbn.TryParse(parts(0), isbn) OrElse
           Not Integer.TryParse(parts(2), NumberStyles.None, CultureInfo.InvariantCulture, year) Then
            Return Nothing
        End If
        Return New Book(parts(1).Trim(), year, isbn)
    End Function

    Sub Main()
        Dim data = "978-0-306-40615-7;Dune;1965" & vbLf &
                   "978-0-306-40615-8;Bad check digit;1999" & vbLf &
                   "9780131103627;The C Programming Language;1988" & vbLf &
                   "not;enough"

        Dim books As New List(Of Book)
        Using reader As New StringReader(data)
            Dim line As String = reader.ReadLine()
            Dim lineNumber As Integer = 1
            Do While line IsNot Nothing
                Dim book = ParseLine(line)
                If book Is Nothing Then
                    Console.WriteLine($"line {lineNumber}: rejected")
                Else
                    books.Add(book)
                End If
                line = reader.ReadLine()
                lineNumber += 1
            Loop
        End Using

        For Each book In books.OrderBy(Function(b) b.Year)
            Console.WriteLine($"{book.Isbn}  {book.Title} ({book.Year})")
        Next
    End Sub
End Module
line 2: rejected
line 4: rejected
9780306406157  Book: Dune (1965) by
9780131103627  Book: The C Programming Language (1988) by

The second row has an invalid check digit and the fourth has too few fields; both are rejected as data, not as exceptions. The accepted rows are ordered by year.

Where the books are outdated

Older Visual Basic books and tutorials teach habits that compile but are wrong today:

Older advice Current practice

Leave Option Strict off "for flexibility", or toggle it per file.

Turn it on in the project; it is the only setting that makes the other rules enforceable.

Use + for string concatenation, And/Or in conditions and IIf as the conditional operator.

&, AndAlso/OrElse and If(a, b, c).

Declare Dim x(10) and describe it as "ten elements".

It declares eleven; the declaration holds the upper bound.

Format and parse dates with the current culture and never mention it.

State the culture, or use an exact format.

"Run On Error Resume Next and test `Err.Number`".

Structured exception handling.

Code samples written for .NET Framework (WebClient, HttpWebRequest, System.Data.SqlClient, BinaryFormatter).

HttpClient, Microsoft.Data.SqlClient and System.Text.Json; see Web, HTTP and Services and Serialization: JSON and XML.

Version notes

  • AndAlso and OrElse have been in the language since Visual Basic .NET 2002 (7.0); Using and IsNot arrived with Visual Basic 2005 (8.0) and the If operator with Visual Basic 2008 (9.0), so there is no version reason to avoid them.

  • Async/Await arrived in Visual Basic 11 (2012); the default value Nothing semantics and case-insensitivity are unchanged since the first version.

  • The SDK default of OptionStrict Off was measured on SDK 10.0.402 with MSBuild property evaluation; the project property is the portable way to turn it on.

  • No language rule on this page changed between Visual Basic 16.9 and 17.13.