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 |
Conversions |
No implicit narrowing. Use |
Strings |
Join text with |
Logic |
Use |
Numbers |
|
Arrays and indexes |
|
Loops and scope |
Initialise every local declared inside a loop. Remember that |
Culture |
Pass a |
Resources |
Wrap every |
Errors |
Use |
Async |
Return |
Libraries |
Do not use default form instances ( |
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 |
|
||
|
Joining a |
|
||
|
|
|
||
|
|
|
||
|
|
Use |
||
Declarations give the upper bound |
|
|
||
|
|
|
||
|
|
|
||
Conversions round halves to even |
|
|
||
Integer arithmetic is checked |
|
Keep the check (it is a feature), or use |
||
1-based runtime functions |
|
|
||
Case-insensitive names |
|
|
||
Loop locals are not reset |
|
|
||
|
|
|
||
Changing a collection in |
|
Iterate over a copy ( |
||
Culture-dependent parsing |
|
|
||
Date literals ignore the culture |
|
Write |
||
Missing |
A |
|
||
|
An exception thrown after an |
|
||
|
|
|
||
|
|
|
||
Default form instances |
|
|
||
Late binding everywhere |
|
Typed variables and interfaces; isolate unavoidable late binding in one small helper (see 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.
Source: Option Strict Statement
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:
-
Andprinted both "evaluated A" and "evaluated B" even though the first operand was alreadyFalse. When the second operand is a null check or has a side effect, that is a bug; withOrand aNothingstring it was aNullReferenceException. -
empty = NothingisTruefor the empty string, because the comparison operator treatsNothingas the empty string forStringoperands.Is Nothingasks the real question, whether the reference is null. For "no text" useString.IsNullOrEmptyorString.IsNullOrWhiteSpace. -
Nothingassigned to a value type is its default value, so anIntegerset toNothingis0and aDateis 1 January 0001. UseInteger?andHasValuewhen "absent" has to be different from zero (see Nothing and Nullable Value Types). -
String comparison with
=is case-sensitive and binary underOption Compare Binary.String.EqualswithStringComparison.OrdinalIgnoreCaseis the explicit way to ignore case, and it does not depend on theOption Comparesetting of the file. -
&converted theIntegerand theNothingstring to text;Nothingbecame an empty string and the interpolated string did the same.&is the concatenation operator;+is addition that happens to work on two strings.
Source: AndAlso Operator
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, soLengthis 4 andUBoundis 3.Dim emptyArray(-1)is the empty array. TheNew Integer(3) {}form uses the same rule. Compute the bound ascount - 1, and readLengthrather thanUBound + 1. See Arrays. -
Division.
/converted twoIntegeroperands toDouble, so7 / 0isInfinityrather than an exception, and7 \ 0throwsDivideByZeroException.\truncates toward zero (-7 \ 2is-3) andModtakes the sign of the left operand (-7 Mod 2is-1). -
Exponentiation.
^always returnsDouble;CInt(power)converts back and throwsOverflowExceptionif the result does not fit. -
Rounding.
CIntandMath.Rounduse banker’s rounding, so2.5becomes2and3.5becomes4.Introunds toward negative infinity (Int(-2.5)is-3) whileFixrounds toward zero (-2). PassMidpointRounding.AwayFromZerowhen the specification says "round half up". See Numbers and Math. -
Overflow is checked.
Integer.MaxValue + 1threwOverflowException; C# wraps unless it is in acheckedcontext. The check can be removed for a whole assembly with theRemoveIntegerChecksproject 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 + 1fails 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)andtext.Substring(6, 5)return the same five characters.InStrreturns0for "not found" and a 1-based position otherwise;IndexOfreturns-1and 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
Diminside 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 (counterprinted 1, 2, 3). Write the initialiser every time, or declare the variable where its lifetime is clear. Reference-type locals are the same: they areNothingonly on the first pass. -
For i = 0 To titles.Count - 1evaluatestitles.Count - 1once. Removing items inside the loop leaves the counter pointing past the end, which threwArgumentOutOfRangeException.RemoveAllwith a predicate is the right tool; so is iterating from the last index to the first. -
For Eachover aList(Of T)detects modification and throwsInvalidOperationExceptionwith the message printed above. -
Identifiers are case-insensitive. In the constructor
Name = namethe parameternamehides the propertyName, so the statement assigns the parameter to itself and the property staysNothing. The compiler reports nothing;Capacityworked only because the code wroteMe.Capacity. Always qualify members withMe.when a parameter has the same name apart from case, or name parameters differently. -
The same case-insensitivity makes a local called
textclash withSystem.Text, and a module calledBeforewith a variable calledbefore(BC30980). The extra rule is "no two names in a scope that differ only by case".
Source: For…Next Statement
Source: Declared Element Names
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/2026is 4 March inen-USand 3 April inen-GB. A program tested on one machine silently misreads dates on another. -
Date.ParseExactwithyyyy-MM-ddandCultureInfo.InvariantCulturedoes not depend on the machine, and the round-trip formatOdoes not either. -
A date literal in source code is always month/day/year, whatever the culture:
#3/4/2026#is 4 March. -
N2formatted the same number as1,234.50,1.234,50and1,234.50in three cultures, and the German text parsed only with the German culture. UseCultureInfo.InvariantCulturefor 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.
Source: Parsing date and time strings
Source: CultureInfo.InvariantCulture
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 |
|---|---|
|
|
The conditional function from Visual Basic 6 ( |
The |
Default form instances ( |
Create the form: |
|
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 |
Hungarian prefixes ( |
The .NET naming guidelines: no type prefixes, PascalCase members, camelCase locals, |
|
Modern Visual Basic gives both variables the type |
|
|
|
Private fields, events and properties; keep the designer file generated. |
Source: Visual Basic Coding Conventions
See also Program Structure and Code Conventions.
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.
Source: Visual Basic Coding Conventions
Where the books are outdated
Older Visual Basic books and tutorials teach habits that compile but are wrong today:
| Older advice | Current practice |
|---|---|
Leave |
Turn it on in the project; it is the only setting that makes the other rules enforceable. |
Use |
|
Declare |
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 |
Structured exception handling. |
Code samples written for .NET Framework ( |
|
Version notes
-
AndAlsoandOrElsehave been in the language since Visual Basic .NET 2002 (7.0);UsingandIsNotarrived with Visual Basic 2005 (8.0) and theIfoperator with Visual Basic 2008 (9.0), so there is no version reason to avoid them. -
Async/Awaitarrived in Visual Basic 11 (2012); the default valueNothingsemantics and case-insensitivity are unchanged since the first version. -
The SDK default of
OptionStrict Offwas 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.
References
-
Visual Basic Coding Conventions, Option Strict Statement and Naming guidelines
-
Concatenation Operators, And Operator, Or Operator, AndAlso Operator, OrElse Operator, Is Operator, IsNot Operator and Integer Division Operator
-
Exponentiation Operator, Type conversion functions, Math.Round and Strings.InStr
-
Arrays, For…Next Statement, For Each…Next Statement and Dim Statement
-
Best practices for comparing strings, Parsing date and time strings and DateTime.Parse
-
Using objects that implement IDisposable, Using Statement and Async
-
On Error Statement, Try…Catch…Finally Statement, Early and late binding and .NET code analysis overview