Program Structure and Code Conventions

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. It describes what a Visual Basic source file may contain and in which order, how the compiler finds the first line of code to run, the lexical rules that decide what counts as a name, a keyword, a comment or a line, and the conventions that Microsoft’s documentation and tooling follow. It is the page to read before writing the first file of the shelf library-catalogue solution that the rest of this section builds on.

The page covers the following, in this order: the shape of a file and of a program, the Imports statement and project-level imports, namespaces and the root namespace, the default access levels of every declaration, the forms of Main and the startup object, the lexical rules (case, identifiers, keywords, line continuation, comments, labels, special characters), the conventions, regions and the documented limits. The Option statements that open every file are described on Option Statements; the variable declaration statements on Variables, Constants and Scope; the project file on Projects, MSBuild and the dotnet CLI; and console programs on Console Applications.

Every example is a complete program. Each one was built on the .NET 10 SDK with Option Strict On, Option Explicit On, Option Infer On and warnings treated as errors, then run, and the output shown is the real one. Examples that are deliberately wrong are labelled and show the compiler’s messages instead.

From solution to statement

A Visual Basic program is built from nested containers. Learn’s wording is that a solution comprises one or more projects, a project produces an assembly, an assembly is compiled from one or more source files, and a source file defines classes, structures, modules and interfaces that contain all the code. Executable code lives only inside procedures: Sub, Function, Operator, property accessors (Get, Set) and the event accessors AddHandler, RemoveHandler and RaiseEvent. A Visual Basic file has no top-level statements: unlike C# 9 and later, a program that is only a list of statements does not compile, and even a three-line program needs a Module and a Sub Main.

The diagram shows the order the compiler requires inside one source file and what each level may contain.

flowchart TD F["Source file (.vb)"] --> O["1. Option statements
Explicit, Strict, Infer, Compare"] O --> I["2. Imports statements
namespaces, aliases, types, XML namespaces"] I --> A["3. Assembly and module attributes
<Assembly: ...>"] A --> N["4. Namespace members"] N --> NS["Namespace blocks (nested)"] N --> T["Types
Class, Structure, Module, Interface"] N --> D["Enum and Delegate"] NS --> T T --> M["Members
Sub, Function, Property, Event, Operator, Declare,
fields, Const, nested types"] M --> S["Statements
only inside procedures, operators and accessors"] C["Conditional compilation directives
#If, #Region, #Const may appear almost anywhere"] -.-> N

The numbers 1 to 4 are an order, not a menu: a statement that arrives early in the wrong place is an error. The messages are the compiler’s own (see the examples further down): Option after any other statement is BC30627, Imports after a declaration or an attribute is BC30465. Directives such as #If, #Const and #Region are the exception and may be placed between the elements; they are described on Conditional Compilation and Directives.

The levels also have names that appear in compiler messages and in Learn: file-level elements (Option, Imports, attributes), namespace-level elements (types, Enum, Delegate, nested Namespace), module-level elements (procedures, properties, events, fields, constants) and procedure-level elements (local variables, local constants and the executable statements). The data elements allowed at each level differ: enumerations and delegates at namespace level, variables, constants, enumerations and delegates at module level, and only local variables and constants inside procedures.

A file with every level

The next program is one file that contains an Option statement, five kinds of Imports, namespaces (including the escape Global), an enumeration, a class, a module and a Main that returns an exit code. The project sets <RootNamespace>Shelf</RootNamespace>, which the output makes visible.

Option Strict On
Option Infer On

Imports System.Collections.Generic
Imports System.Math
Imports Cat = Shelf.Catalog
Imports Titles = System.Collections.Generic.List(Of String)
Imports <xmlns:b="urn:shelf:books">

Namespace Catalog
    Public Enum LoanState
        Available
        OnLoan
    End Enum

    Public Class Book
        Public ReadOnly Property Title As String
        Public Property State As LoanState

        Public Sub New(title As String)
            Me.Title = title
        End Sub

        Public Overrides Function ToString() As String
            Return Title & " (" & State.ToString() & ")"
        End Function
    End Class
End Namespace

Namespace Global.Elsewhere
    Public Module Tools
        Public Function Describe(item As Object) As String
            Return item.GetType().FullName
        End Function
    End Module
End Namespace

Module Program
    Function Main(args As String()) As Integer
        Dim book As New Cat.Book("Dune")
        book.State = Cat.LoanState.OnLoan
        Dim titles As New Titles From {book.Title, "Emma"}
        Dim entry = <b:book isbn="9780441013593"><b:title><%= book.Title %></b:title></b:book>

        Console.WriteLine(book)
        Console.WriteLine(Elsewhere.Tools.Describe(book))
        Console.WriteLine(Elsewhere.Tools.Describe(New Object()))
        Console.WriteLine(GetType(Program).FullName)
        Console.WriteLine(titles.GetType().Name & " has " & titles.Count.ToString() & " titles")
        Console.WriteLine("Max(3, 7) = " & Max(3, 7).ToString() & ", Abs(-2) = " & Abs(-2).ToString())
        Console.WriteLine(entry.Name.NamespaceName & " " & entry.Name.LocalName)
        Console.WriteLine("arguments: " & args.Length.ToString() & " -> " & String.Join(" | ", args))
        Return args.Length
    End Function
End Module

Run with the arguments one "two words" 3:

Dune (OnLoan)
Shelf.Catalog.Book
System.Object
Shelf.Program
List`1 has 2 titles
Max(3, 7) = 7, Abs(-2) = 2
urn:shelf:books book
arguments: 3 -> one | two words | 3

The process exit code was 3, the number of arguments, because Main is a Function that returns an Integer. Source: Structure of a Visual Basic Program

Four lines of the output carry the lesson:

  • Shelf.Catalog.Book: Namespace Catalog was written inside a file whose project has the root namespace Shelf, so the type is Shelf.Catalog.Book. The root namespace is prepended to every Namespace statement.

  • Namespace Global.Elsewhere declares the namespace Elsewhere, not Shelf.Elsewhere: Global starts the name at the very top, outside the root namespace, which is why Main calls Elsewhere.Tools.Describe without the Shelf prefix. (Describe reports System.Object for the argument New Object(), the line that follows the Book one.)

  • Shelf.Program: a type declared outside any Namespace statement belongs to the root namespace.

  • urn:shelf:books book: the XML namespace import gave the prefix b a meaning at file level, and the XML literal <b:book> carries it.

Imports

Imports makes names visible without qualification. It never loads an assembly, never changes what a name means and has no run-time effect: an assembly comes in through a package or project reference, and Imports only shortens the names inside it. Visual Basic has five forms:

Form Example Effect

Namespace

Imports System.Collections.Generic

Every type of the namespace can be used by its simple name.

Namespace alias

Imports Cat = Shelf.Catalog

Cat.Book means Shelf.Catalog.Book. Useful for long names and for removing an ambiguity.

Type alias

Imports Titles = System.Collections.Generic.List(Of String)

A constructed generic type gets a short name. An alias written as a statement is local to its file.

Type or namespace members

Imports System.Math

The shared members of a type (here Max, Abs, PI) can be used without the type name. It is the counterpart of C# using static.

XML namespace

Imports <xmlns:b="urn:shelf:books">

The prefix b and, with xmlns=, the default namespace can be used in XML literals and axis properties.

Rules worth keeping in mind:

  • When two imported namespaces contain a type with the same simple name, the simple name is ambiguous (BC30561) and has to be qualified or aliased. An alias is the shortest fix.

  • Importing System.Math or System.Console as a type removes the need to write the type name, and also removes the signal it gave: Max(3, 7) looks like the call of a function of the program. Use it for mathematical code, not for general purpose names.

  • The XML namespace form is described on XML Literals and Axis Properties.

Project-level imports and the SDK defaults

A name that every file needs can be imported once, for the whole project. In an SDK-style project the item is Import, and the build passes the list to vbc as the -imports option. The next project adds two imports and the program uses names from them without any Imports statement of its own:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>Shelf</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <Import Include="System.Text" />
    <Import Include="Culture=System.Globalization.CultureInfo" />
  </ItemGroup>
</Project>
Module Program
    Sub Main()
        ' System, System.Collections.Generic, System.Linq and Microsoft.VisualBasic come from the SDK's default imports.
        Dim names As New List(Of String) From {"Emma", "Dune"}
        Dim builder As New StringBuilder()
        For Each name In names.OrderBy(Function(n) n)
            builder.Append(name).Append(" ")
        Next
        Console.WriteLine(builder.ToString().Trim())
        Console.WriteLine(Culture.InvariantCulture.EnglishName)
        Console.WriteLine(Len("Shelf"))
    End Sub
End Module
Dune Emma
Invariant Language (Invariant Country)
5

Source: -imports

The second Import is a project-wide alias, written Alias=TypeOrNamespace in the item’s Include. This form is not described on the -imports page of the documentation; it is what vbc accepts. Writing the alias as item metadata (<Import Include="System.Globalization.CultureInfo" Alias="Culture" />) did not work in the build that was tried: the metadata was ignored and the members of CultureInfo were imported instead, which made unrelated names ambiguous. Use the Culture=... form shown above, or avoid project-level aliases.

The SDK also imports a fixed set of namespaces into every Visual Basic project, which is why the program above uses List, Console, OrderBy and Len without any Imports. On SDK 10.0.402 the compiler command line of a console project contained -imports with: Microsoft.VisualBasic, System, System.Collections, System.Collections.Generic, System.Diagnostics, System.Linq, System.Xml.Linq and System.Threading.Tasks. An Import Remove item for one of these (System.Xml.Linq was tried) left it in the list in the build that was inspected. Consequences:

  • A file does not need Imports System or Imports System.Linq. The dotnet new console -lang vb template still emits Imports System at the top of Program.vb; it is harmless and redundant.

  • A class named Task, Debug or Enumerable in your project is shadowed by, or ambiguous with, the imported type depending on the namespaces involved. Name your types so that they do not collide with these eight namespaces.

  • Code copied from a project with another set of imports (a .NET Framework project, a Visual Studio template with My Extensions) may need Imports statements to compile in a library project.

Namespaces

Namespace groups types and gives them a qualified name. The name is prefixed with the project’s root namespace, the MSBuild property RootNamespace (the default is the project name, and dotnet new writes it into the project file). Three consequences:

  1. The dotted name in a Namespace statement is relative to the root namespace: Namespace Catalog in the Shelf project is Shelf.Catalog. Repeating the root (Namespace Shelf.Catalog) gives Shelf.Shelf.Catalog, one of the most common surprises when moving from C#, where the namespace is written in full.

  2. Types outside any Namespace statement are in the root namespace itself. With an empty root namespace, they are in the global namespace.

  3. Namespace Global.Elsewhere opts out of the root namespace and declares the namespace Elsewhere. In expressions, Global.System.String starts the lookup at the global namespace, and is the way to reach System when some enclosing namespace has a member of the same name.

A namespace declared in the project as System or Collections under a root namespace can hide the framework one. In an experiment, a file with Namespace System inside the Shelf root made the compiler fail on the generated AssemblyInfo.vb with BC31424 ("Type 'System.Reflection.AssemblyCompanyAttribute' …​ has been forwarded to assembly 'System.Runtime'") because the generated file’s System resolved to Shelf.System. Do not name project namespaces after framework ones.

Other facts:

  • Namespaces are open: the same namespace can be declared in many files and projects, and its members merge.

  • Namespace blocks nest, and Namespace A.B is the same as Namespace A containing Namespace B.

  • Only namespace-level elements go inside a namespace: types, enumerations, delegates and nested namespaces. A Sub or a variable directly in a namespace is BC30001 ("Statement is not valid in a namespace.").

  • The Imports statements apply to the whole file, whichever namespace block follows; there is no per-namespace Imports.

  • A namespace is not a run-time entity; it is only a prefix of the names of its types.

Default access levels

A declaration without an access modifier gets the default of its kind. The next program prints the real defaults with reflection, for a class, a structure, a module, an interface and an enumeration declared with no modifiers at all:

Option Strict On

Imports System.Reflection

Class DefaultType
    Dim plainField As Integer
    Const Limit As Integer = 3
    Sub DoWork()
    End Sub
    Function Compute() As Integer
        Return 1
    End Function
    Property Name As String
    Event Changed As EventHandler
    Sub New()
    End Sub
End Class

Structure DefaultStructure
    Dim plainField As Integer
    Sub DoWork()
    End Sub
End Structure

Module DefaultModule
    Dim plainField As Integer
    Sub DoWork()
    End Sub
End Module

Interface IDefault
    Sub DoWork()
End Interface

Enum DefaultEnum
    First
End Enum

Class [MyClass]
End Class

Module Program
    Private Function Access(info As MethodBase) As String
        If info.IsPublic Then Return "Public"
        If info.IsPrivate Then Return "Private"
        If info.IsAssembly Then Return "Friend"
        If info.IsFamily Then Return "Protected"
        Return "other"
    End Function

    Private Function TypeAccess(t As Type) As String
        Return If(t.IsPublic, "Public", If(t.IsNotPublic, "Friend", "nested"))
    End Function

    Private Sub Dump(t As Type)
        Console.WriteLine(t.Name & " -> " & TypeAccess(t))
        Const all As BindingFlags = BindingFlags.Instance Or BindingFlags.Static Or BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.DeclaredOnly
        For Each f In t.GetFields(all)
            Dim kind = If(f.IsLiteral, "constant", "field")
            Dim vis = If(f.IsPublic, "Public", If(f.IsPrivate, "Private", If(f.IsAssembly, "Friend", "other")))
            Console.WriteLine("    " & kind & " " & f.Name & " = " & vis)
        Next
        For Each m In t.GetMethods(all)
            Console.WriteLine("    " & If(m.IsSpecialName, "accessor", "method") & " " & m.Name & " = " & Access(m))
        Next
        For Each c In t.GetConstructors(all)
            Console.WriteLine("    constructor = " & Access(c))
        Next
    End Sub

    Sub Main()
        Dump(GetType(DefaultType))
        Dump(GetType(DefaultStructure))
        Dump(GetType(DefaultModule))
        Console.WriteLine(GetType(IDefault).Name & " -> " & TypeAccess(GetType(IDefault)))
        Console.WriteLine(GetType(DefaultEnum).Name & " -> " & TypeAccess(GetType(DefaultEnum)))
        Console.WriteLine(GetType([MyClass]).Name)
    End Sub
End Module
DefaultType -> Friend
    field plainField = Private
    field _Name = Private
    field ChangedEvent = Private
    constant Limit = Private
    method DoWork = Public
    method Compute = Public
    accessor get_Name = Public
    accessor set_Name = Public
    accessor add_Changed = Public
    accessor remove_Changed = Public
    constructor = Public
DefaultStructure -> Friend
    field plainField = Public
    method DoWork = Public
DefaultModule -> Friend
    field plainField = Private
    method DoWork = Public
IDefault -> Friend
DefaultEnum -> Friend
MyClass

The pattern:

  • Every type declared at namespace level is Friend (the CLR’s internal): visible inside the assembly only. Public Class has to be written for a library.

  • Sub, Function, Property, Event and Sub New default to Public: the accessor lines of the output are the property and event accessors, and they are public too. This is the opposite of C#, where members default to private.

  • A field declared with Dim is Private in a class and a module, but Public in a structure. Const in a class is Private. Always write Private or Public on a field.

  • The Event in the output is stored in a private field named ChangedEvent, and an auto-property named Name in a private field _Name. Both fields are generated and unrelated to your own naming.

  • Interface members and enumeration members are Public, and an interface member cannot carry an access modifier.

The class is also named with an escaped identifier on purpose: Class [MyClass] compiles because MyClass is a reserved word that the brackets turn into a plain name (see below).

The Main procedure

A project that builds an executable (OutputType Exe) must have exactly one entry point named Main. Visual Basic accepts four signatures, written as a Sub or a Function:

Declaration Meaning

Sub Main()

No arguments, exit code 0 unless Environment.ExitCode or an unhandled exception says otherwise.

Sub Main(args As String())

Receives the command-line arguments. This is what dotnet new console -lang vb generates, inside Module Program.

Function Main() As Integer

The returned value is the process exit code.

Function Main(args As String()) As Integer

Both.

The rules around them, each verified with the compiler:

  • Main can be in a Module (the form used throughout this section) or be a Shared member of a class or structure. In a Module the Shared keyword is implicit. Unlike C#, the procedure must be accessible: Private Shared Function Main is rejected with BC30737, so use Public or Friend.

  • The name is case-insensitive (main, MAIN), and the array does not contain the program name: the three arguments of the first example arrived as three elements.

  • There is no Async Main. Async Function Main() As Task is BC30737 and a program that needs Await at the top calls an async method with .GetAwaiter().GetResult() from a synchronous Main (see Async and Await). This is a difference from C# 7.1 and later.

  • A Windows Forms project with the Application Framework enabled has a generated Main and the project’s MyType is WindowsForms. If you write your own Sub Main in such a project, set MyType to WindowsFormsWithCustomSubMain (see Windows Forms Applications).

  • A class library has no Main.

  • When more than one Main exists, the build needs to know which one to start, and nothing but the property StartupObject (the -main option of vbc) decides.

A shared Main in a class, with the explicit access modifier it requires, returning an exit code:

Option Strict On

Public NotInheritable Class Launcher
    Private Sub New()
    End Sub

    ' In a class, Main must be Shared. In a Module it would not need the keyword.
    Public Shared Function Main(args As String()) As Integer
        Console.WriteLine("entry point: " & GetType(Launcher).FullName)
        Console.WriteLine("exit code requested: " & Exitcode(args).ToString())
        Return Exitcode(args)
    End Function

    Private Shared Function Exitcode(args As String()) As Integer
        Return If(args.Length = 0, 0, 64)
    End Function
End Class
entry point: Shelf.Launcher
exit code requested: 0

Source: -main

The entry-point failures appear as errors of the compiler and not of the language, with no line number. These are the real messages for the four cases. The first one has two Main procedures in Module Importer and Module Reporter:

Option Strict On

Module Importer
    Sub Main()
        Console.WriteLine("importer")
    End Sub
End Module

Module Reporter
    Sub Main()
        Console.WriteLine("reporter")
    End Sub
End Module
vbc : error BC30738: 'Sub Main' is declared more than once in 'TwoMains': TwoMains.Importer.Main(), TwoMains.Reporter.Main()
vbc : error BC30737: No accessible 'Main' method with an appropriate signature was found in 'AsyncMain'.
vbc : error BC30420: 'Sub Main' was not found in 'NoEntry'.
vbc : error BC30737: No accessible 'Main' method with an appropriate signature was found in 'InstanceMain'.

The second line is the Async Function Main() As Task file (BC30737), the third a module with a Sub Start and no Main at all (BC30420), and the fourth a class with a non-Shared Sub Main (BC30737). The project that builds the first listing is repaired by naming the module in StartupObject. Visual Studio’s Application type and Startup object boxes on the project’s Application page edit the same properties:

<StartupObject>Shelf.Reporter</StartupObject>

With that property in the project (root namespace Shelf, same source file), the program starts Reporter.Main:

reporter

The value is the full name, including the root namespace. A value that is only the module name (Reporter) fails with BC30420 ("'Sub Main' was not found in 'Reporter'"), because the root namespace is part of the type’s name.

Lexical rules

The compiler first turns the characters of a file into tokens and then into logical lines. Learn and the specification describe the grammar in the chapter Lexical Grammar. The next program exercises the rules in this section; its output is shown after it.

Option Strict On

Imports System.Collections.Generic
Imports System.Linq

Module Program
    Private Function [Select](items As IEnumerable(Of String), [Class] As String) As IEnumerable(Of String)
        Return items.Where(Function(item) item.StartsWith([Class], StringComparison.Ordinal))
    End Function

    Sub Main()
        ' Identifiers are case-insensitive; the declared spelling is what tooling displays.
        Dim shelfCount As Integer = 3
        SHELFCOUNT += 1
        Console.WriteLine("shelfCount = " & ShelfCount.ToString())
        console.writeline("this also compiles")

        ' Unicode letters are legal in identifiers, and so is a leading underscore plus a letter.
        Dim año As Integer = 2026
        Dim _draft As String = "v1"
        Console.WriteLine(año.ToString() & " " & _draft)

        ' A reserved word can be used as a name when it is escaped with square brackets.
        Dim [Loop] As String = "ring"
        Dim titles As String() = {"Dune", "Dubliners", "Emma"}
        Console.WriteLine([Loop] & ": " & String.Join(", ", [Select](titles, "Du")))

        ' Implicit line continuation: after a comma, an operator, an open bracket and
        ' before a close bracket, and before and after a query operator.
        Dim summary = String.Join(
            ", ",
            titles.Where(Function(t) t.Length > 4
                         ).OrderBy(Function(t) t))
        Dim total = shelfCount +
            2 *
            5
        Dim longTitles = From t In titles
                         Where t.Length > 4
                         Order By t Descending
                         Select t
        Console.WriteLine(summary & " | " & total.ToString() & " | " & String.Join("+", longTitles))

        ' Explicit continuation with _ ; since Visual Basic 16 a comment may follow it.
        Dim padded = "[" & _ ' the opening bracket
            titles(0).PadRight(8) & _ ' the title
            "]"
        Console.WriteLine(padded)

        ' Several statements on a physical line: legal, but Microsoft's conventions say not to.
        Dim x As Integer = 1 : Dim y As Integer = 2 : x += y
        Console.WriteLine("x = " & x.ToString())

        REM Old-style comment; the keyword REM works only at the start of a statement.
        Dim attempts As Integer = 0
Retry:
        attempts += 1
        If attempts < 3 Then GoTo Retry
        Console.WriteLine("attempts = " & attempts.ToString())
    End Sub
End Module
shelfCount = 4
this also compiles
2026 v1
ring: Dune, Dubliners
Dubliners | 14 | Dubliners
[Dune    ]
x = 3
attempts = 3

Case insensitivity

Identifiers and keywords are case-insensitive: ShelfCount, SHELFCOUNT and shelfcount are the same name, and so are Console and console. The Unicode simple one-to-one case mappings are used and locale-specific mappings are ignored, so the Turkish dotted and dotless i do not cause trouble. Two declarations that differ only in case in the same scope are a duplicate:

Option Strict On

Module Program
    Sub Main()
        Dim Count As Integer = 1
        Dim COUNT As Integer = 2
        Console.WriteLine(count)
    End Sub
End Module
Program.vb(6,13): error BC30288: Local variable 'COUNT' is already declared in the current block.

The same rule produces BC30269 ("has multiple definitions with identical signatures") for Sub Print() and Sub PRINT() in one module. The Visual Studio editor rewrites the case of every use of a name to the case of its declaration while you type, which is why VB code looks uniformly cased. The case-insensitivity has two practical consequences for a team that also writes C#:

  • A C# library that exposes two public members differing only in case (Name and name) is not CLS-compliant and cannot be used reliably from Visual Basic, because VB cannot tell the two apart. Keep public names unique ignoring case.

  • Case-insensitivity applies to names only. String contents, XML element names in literals and Dictionary keys remain case-sensitive (unless Option Compare Text or a comparer says otherwise).

Identifiers

An identifier starts with a letter or with an underscore that is followed by at least one more identifier character (a lone _ would be a line continuation, and Dim _ As Integer is BC30203). The remaining characters can be letters, decimal digits, combining marks, formatting characters and underscores. Letters are those of Unicode classes Lu, Ll, Lt, Lm, Lo and Nl, so Dim año As Integer and Greek or Cyrillic names are legal, as the program shows. Names that start with an underscore are not CLS-compliant, which matters only for public members; the private-field style _title is common and fine. Identifiers can carry a type character (name$, count%), described on Data Types and Literals, and no identifier may carry one after being escaped.

Escaped identifiers use square brackets so that a reserved word can be a name: [Select], [Class], [End], [Loop]. In the program above, the method is [Select] and its parameter is [Class]; the brackets are needed at the declaration and at each use, and they are not part of the name. After a dot a reserved word is accepted as a member name without brackets: GetType(String).Module and System.Linq.Enumerable.Select(words, ...) compile on SDK 10.0.402, which is how members of the base class library whose names are Visual Basic keywords are called. Brackets are only needed where the word would otherwise start a statement or declare a name. A type named MyClass is written [MyClass], as in the access-level listing above.

Keywords

The Visual Basic Language Specification lists 152 reserved words (the table in the Keywords section of the lexical-grammar chapter, counted from the grammar), among them Alias, AndAlso, ByRef, CType, Handles, IsNot, Narrowing, NotInheritable, MyClass, Overloads, Partial, Shadows, Widening and the conversion keywords CBool to CUShort. The specification retains EndIf, GoSub, Let, Variant and Wend as reserved although the language no longer uses them. A reserved word cannot be a name unless it is escaped:

Option Strict On

Imports System.Reflection

<Assembly: AssemblyTitle("Shelf")>

Imports System.Text

Module Program
    Sub Main()
        Dim _ As Integer = 3
        Dim end As Integer = 4
        Dim class As Integer = 5
    End Sub
End Module
Program.vb(11,13): error BC30203: Identifier expected.
Program.vb(12,13): error BC30183: Keyword is not valid as an identifier.
Program.vb(13,13): error BC30183: Keyword is not valid as an identifier.
Program.vb(7,1): error BC30465: 'Imports' statements must precede any declarations.

That listing shows three more errors in one go: the lone underscore (BC30203), End and Class used as names (BC30183) and the Imports statement placed after an attribute (BC30465). The compiler reports the errors of the lexical phase first, which is why the order is not the order of the lines.

Many words that look like keywords are contextual: they have a meaning only in one syntactic place and are legal names elsewhere. A test compiled local variables with each of the names Aggregate, Ansi, Assembly, Async, Auto, Await, Binary, Compare, Custom, Distinct, Equals, Explicit, From, Group, Infer, Into, Iterator, Join, Key, Mid, Off, Order, Out, Preserve, Region, Skip, Strict, Take, Text, Unicode, Until, Where and Yield without any error on SDK 10.0.402, while Overloads and Partial were rejected with BC30183. Legal does not mean wise: the names that belong to the query syntax (From, Where, Order, Group, Join, Into, Distinct, Take, Skip) and to Async/Await/Iterator/Yield read badly as variable names. The practical test of any word is the compiler.

Line terminators and line continuation

A statement ends at the end of the physical line, unless the line is continued. Two mechanisms exist:

  • Implicit line continuation, introduced in Visual Basic 10 (2010). The line may be broken, without any character, after a comma, an open parenthesis, brace or embedded expression <%=, after a member qualifier . that qualifies something, before a close parenthesis, brace or embedded expression, after a binary operator in an expression and after an assignment operator, around query operators (Where, Order By, Select, …​) and in attribute lists (after < and before >).

  • Explicit line continuation: a space followed by an underscore as the last character of the line. Visual Basic 16 (2019) allows a comment after the underscore, as the program above does (& _ ' the opening bracket). Learn’s page on comments still says that a comment cannot follow a continuation; the compiler of SDK 10.0.402 accepts it.

Implicit continuation does not combine with an explicit one: Learn’s specification shows that Dim x = _ followed by y on the next line is an error, because the underscore is not a place where implicit continuation applies. Implicit continuation is also not available inside conditional-compilation conditions, because the text in an excluded block need not be valid. Everything else is a matter of style: Microsoft’s conventions say to prefer implicit continuation and avoid the _ where the language allows it, which is the style used in this section. The reverse risk is a statement that looks continued but is not. A line that ends in a value is complete, so a next line that begins with an operator is a new statement:

Dim total = first
    + second

The compiler reports BC30035 ("Syntax error.") on the second line of that fragment, which it reads as a new statement that starts with a unary plus.

Put the operator at the end of the line, as Dim total = first + followed by second, so that the line cannot be mistaken for finished. In a query, every clause starts a new line without any character, and the clauses can be indented under From.

Statement separator, labels and the one-line If

The colon : joins statements on one physical line (Dim x As Integer = 1 : Dim y As Integer = 2 : x += y). Microsoft’s conventions say to use one statement per line. The colon also ends a label: Retry: at the start of a line names a statement for GoTo. A label is an identifier or an integer (line numbers from Visual Basic 6 are labels), may be followed by a statement on the same line, and is local to the procedure. GoTo is legal but rarely needed; the structured alternatives are on Control Flow. The single-line If condition Then statement : statement treats everything after Then to the end of the line as the Then branch, so an Else follows on the same line; this is one of the places where the colon changes meaning and makes code harder to read.

Comments

A comment starts with an apostrophe ' or with the keyword REM and ends at the end of the physical line. The comment marker may be any of three characters: the ASCII apostrophe, the left single quotation mark U+2018 and the right single quotation mark U+2019. The Unicode ones exist because word processors insert them, and pasted code from a document therefore comments out the line where the quote was meant to be a string delimiter: Visual Basic strings use the double quote only. REM works at the start of a line and also after a statement on the same line (Dim a As Integer = 1 REM note and Dim b As Integer = 2 : REM note both compiled), but the apostrophe is the form that Learn and the tooling expect. There are no block comments. ''' starts an XML documentation comment, which is the subject of XML Documentation, Analyzers and Code Style. A comment cannot continue on the next line with an underscore.

Special characters

The following program uses the characters that are not letters or digits and that beginners find confusing, among them the dictionary-access operator !, named arguments :=, the null-conditional ?., the compound assignments and the doubling of a double quote in a string:

Option Strict On

Imports System.Collections.Generic

Module Program
    Function Pad(text As String, Optional width As Integer = 10, Optional fill As Char = "."c) As String
        Return text.PadRight(width, fill)
    End Function

    Sub Main()
        Dim stock As New Dictionary(Of String, Integer) From {{"Dune", 3}, {"Emma", 1}}

        ' ! is the dictionary-access operator: stock!Dune means stock("Dune") (the default property).
        Console.WriteLine("copies of Dune: " & stock!Dune.ToString())
        stock!Emma += 4
        Console.WriteLine("copies of Emma: " & stock("Emma").ToString())

        ' := names an argument, and the names can come in any order.
        Console.WriteLine(Pad("Title", fill:="-"c, width:=8) & "|")

        ' ?. short-circuits on Nothing and gives Nothing (or a Nullable) instead of throwing.
        Dim missing As String = Nothing
        Console.WriteLine("length of missing: " & If(missing?.Length.ToString(), "none"))

        ' Compound assignment operators: &=  +=  -=  *=  /=  \=  ^=  <<=  >>=
        Dim label As String = "shelf"
        label &= " 7"
        Dim total As Integer = 17
        total \= 5
        Dim power As Double = 2
        power ^= 10
        Console.WriteLine(label & " | " & total.ToString() & " | " & power.ToString())

        ' Double quotes inside a string are doubled; there are no backslash escapes.
        Console.WriteLine("She said ""hello"" in C:\temp\shelf")
        Console.WriteLine($"{"Dune",-8}|{3,4}|{19.5R,8:F2}|")
    End Sub
End Module
copies of Dune: 3
copies of Emma: 5
Title---|
length of missing: none
shelf 7 | 3 | 1024
She said "hello" in C:\temp\shelf
Dune    |   3|   19.50|
Character(s) Use

( )

Argument lists, grouping, indexing of arrays (arrays use parentheses, not brackets).

{ }

Array and collection initializers, From {...} and With {...}, and the holes of an interpolated string.

.

Member access; a leading . inside With refers to the With object.

!

Dictionary access (stock!Dune is stock("Dune")) when the type has a default property with one String parameter. Also the Single type character.

?. and ?(

Null-conditional member and index access.

: and :=

Statement separator and label terminator; named argument.

& and &=

String concatenation. & also introduces the radix literals &H and &O and &B, and is the Long type character.

\ and ^

Integer division and exponentiation. The backslash in a string is an ordinary character.

' and REM

Comments.

_

Explicit line continuation (last character of a line) and a leading or embedded character of an identifier.

[ ]

Escaped identifiers.

#

Date literals (#10/19/2026#), directives (#If, #Region) and the Double type character.

$"..."

Interpolated string.

@

Decimal type character, and the attribute-axis marker in XML (element.@name).

<%= %>

Embedded expression in an XML literal.

The ! operator, the XML ones and the date-literal forms are covered on Collections, XML Literals and Axis Properties and Dates, Times and Formatting.

Naming and coding conventions

Microsoft’s documentation and samples follow a set of conventions that Learn publishes as the Visual Basic Coding Conventions and the Visual Basic Naming Conventions. They are suggestions, not rules enforced by the compiler, and the .NET design guidelines for names apply to the public surface of a library. The table summarises the ones that matter for readable code; the reference solution of this section follows them.

Topic Convention Example

Types, members, properties, events, enumerations

PascalCase: start each word with a capital letter. Types and properties are nouns, methods begin with a verb.

LoanState, FindOverdue, Title

Locals and parameters

camelCase.

borrower, daysLate

Private fields

An underscore plus camelCase is the usual team convention; the official page does not prescribe it.

_title

Interfaces

Prefix I, then a noun or an adjective; no underscore.

ILendable

Event arguments, handlers and events

Suffix EventArgs for argument classes and EventHandler for delegates; present or past tense for before and after.

OverdueEventArgs, Overdue, Returned

Hungarian prefixes

Do not use them: strTitle, intCount, clsLoan, frmMain. The type is visible and checked by the compiler.

title, count, Loan

My in a name

Do not use My or my as part of a variable name; it confuses readers with the My namespace.

not myLoan

Abbreviations

Use them sparingly and consistently; a well-known one (Html, Id) is fine.

IsbnLookup

Scope shadowing

Avoid an inner-scope name that equals an outer-scope name.

a local Date hides the Date type name

The layout and language conventions of the coding-conventions page, as a checklist:

  • Indent by four spaces and insert tabs as spaces; one statement and one declaration per line; no : separator.

  • Prefer implicit line continuation over _; align the items of a continued list.

  • Put comments on their own line, start them with a capital letter, end them with a period, put one space after the ', and do not frame comments with asterisks.

  • Use string interpolation for short strings and StringBuilder for appending in loops. Use & and never + to join text.

  • Use Integer rather than unsigned types unless they are necessary; use the short array form Dim names As String() = {...}, with the array designator on the type.

  • Use With for a series of calls on one object, IsNot Nothing instead of Not ... Is Nothing, short New instantiation (Dim book As New Book()) and object initializers.

  • Use Try…​Catch and Using; do not use On Error. Do not test an event for Nothing before RaiseEvent; the statement does it.

  • In event handlers, use Handles rather than AddHandler, and use AddressOf without creating the delegate explicitly. If the handler ignores the arguments, leave them out with relaxed delegate conversion.

  • Call Shared members by the type name, not through an instance.

  • Use XML literals and axis properties for XML, and Where before other query clauses, Join rather than an implicit join, and inferred types for range variables.

The next program follows these conventions in one place: an I interface, an EventArgs class, a Private field with an underscore, an event, an object initializer, With, IsNot Nothing, interpolation, StringBuilder, a #Region and an event handler with relaxed arguments.

Option Strict On
Option Infer On

Imports System.Text

Namespace Lending
    Public Interface ILendable
        ReadOnly Property IsAvailable As Boolean
        Sub Lend(borrower As String)
    End Interface

    Public Class OverdueEventArgs
        Inherits EventArgs

        Public ReadOnly Property DaysLate As Integer

        Public Sub New(daysLate As Integer)
            Me.DaysLate = daysLate
        End Sub
    End Class

    Public Class Loan
        Implements ILendable

#Region "State"
        Private ReadOnly _title As String
        Private _borrower As String
        Private _overdueDays As Integer
#End Region

        Public Event Overdue As EventHandler(Of OverdueEventArgs)

        Public Sub New(title As String)
            _title = title
        End Sub

        Public ReadOnly Property IsAvailable As Boolean Implements ILendable.IsAvailable
            Get
                Return _borrower Is Nothing
            End Get
        End Property

        Public Sub Lend(borrower As String) Implements ILendable.Lend
            _borrower = borrower
        End Sub

        Public Sub MarkLate(days As Integer)
            _overdueDays = days
            RaiseEvent Overdue(Me, New OverdueEventArgs(days))
        End Sub

        Public Function Describe() As String
            Dim state = If(IsAvailable, "available", $"lent to {_borrower}")
            Return $"{_title}: {state}"
        End Function
    End Class

    Public Class Reminder
        Public Property Prefix As String = "Reminder"
        Public Property Sent As Integer
    End Class
End Namespace

Module Program
    Private WithEvents CurrentLoan As Lending.Loan

    ' Relaxed delegate: a Handles method may leave out the sender and e parameters.
    Private Sub OnOverdue() Handles CurrentLoan.Overdue
        Console.WriteLine("overdue handler ran")
    End Sub

    Sub Main()
        CurrentLoan = New Lending.Loan("Dune")
        Dim loan = CurrentLoan
        Console.WriteLine(loan.Describe())
        loan.Lend("Ana")
        Console.WriteLine(loan.Describe())

        loan.MarkLate(3)

        Dim reminder As New Lending.Reminder With {.Prefix = "Overdue notice"}
        With reminder
            .Sent += 1
            Console.WriteLine($"{.Prefix}: {.Sent} sent")
        End With

        Dim titles As String() = {"Dune", "Emma", "Ulysses"}
        Dim builder As New StringBuilder()
        For Each title In titles
            builder.Append(title).Append(";"c)
        Next
        Console.WriteLine(builder.ToString())

        If loan IsNot Nothing AndAlso Not loan.IsAvailable Then
            Console.WriteLine("still on loan")
        End If
    End Sub
End Module
Dune: available
Dune: lent to Ana
overdue handler ran
Overdue notice: 1 sent
Dune;Emma;Ulysses;
still on loan

Two details of that program are easy to get wrong. The relaxed handler OnOverdue() works because it is bound with Handles on a WithEvents variable. The same method bound with AddHandler loan.Overdue, AddressOf OnOverdue does not compile under Option Strict On: the compiler reports BC36663, "Option Strict On does not allow narrowing in implicit type conversions between method 'Private Sub OnOverdue()' and delegate 'Delegate Sub EventHandler(Of OverdueEventArgs)(sender As Object, e As OverdueEventArgs)'". The same happens when a function without parameters is converted with AddressOf to a Func(Of Integer, Integer). A lambda that ignores its parameters (Sub() ...) is accepted. The rules for delegates are on Delegates and Lambda Expressions and for events on Events and Handlers.

Compiler and editor settings can enforce part of the conventions. The .editorconfig file supports dotnet_naming_rule, dotnet_naming_symbols and dotnet_naming_style entries for Visual Basic, and dotnet_diagnostic.<id>.severity for analyzer rules. The tools are covered on XML Documentation, Analyzers and Code Style. Generated code (designer files, My.Settings) does not have to be renamed to fit the guidelines.

Regions and outlining

#Region "name" and #End Region mark a block that the editor can collapse. They have no run-time effect and may contain any elements of the surrounding level. The program above has a region around three fields. Learn documents BC32025 (a region inside a method body or a multiline lambda); a region around statements in an ordinary Sub compiled on SDK 10.0.402, so treat the restriction as dependent on the context. Regions help with generated code and with long files of constants; wrapping every method group in a region, however, is now widely regarded as a smell, because a long class is a signal to split it. The directive is documented on the page of the language reference for #Region, and the other directives on Conditional Compilation and Directives.

Limits

The Visual Basic compiler on .NET has few fixed limits, and Learn’s Limitations page lists those that can matter in extreme cases: a maximum for the length of a declared name (applied to the whole qualified string), 65535 characters in a physical line of source (a longer logical line is possible with continuation), a maximum for the number of dimensions of an array, a maximum string length, and 32768 characters for an environment string used as a command-line argument. Physical limits are mostly memory. Earlier Visual Basic versions limited module size and the number of variables; Visual Basic .NET relaxed those restrictions.

Errors on this page

Code Cause

BC30627

An Option statement after another statement or inside a type.

BC30465

An Imports statement after a declaration or an attribute.

BC30738

Two Main procedures and no StartupObject.

BC30737

No Main with a suitable signature: non-Shared, Private, or an Async Function.

BC30420

An executable with no Main, or a StartupObject that names a type without one.

BC30183

A reserved word used as a name without brackets.

BC30203

A lone underscore, or a declaration with a missing identifier.

BC30001

A Sub, Function or variable declared directly in a Namespace.

BC30035

A line that starts with an operator because the previous line was already complete.

BC30288

A name declared twice in the same block, ignoring case.

BC30269

Two members with the same name ignoring case and the same signature.

BC30205

Extra tokens after a complete statement, as in Public Class Dog Inherits Animal on one line.

BC30561

An ambiguous simple name from two imported namespaces.

BC31424

A project namespace that hides System for generated code.

The error numbers above have no page of their own on this site; the messages shown in the listings are the compiler’s output, and the documentation index of Visual Basic errors is Visual Basic error messages.

Differences from C#

Concern Visual Basic C#

Top-level statements

None. A Module with Sub Main is the minimum.

C# 9 and later.

Main

Sub or Function; Shared in a class; must not be Private; no async form.

static, any access; async Task Main since C# 7.1.

Case

Insensitive for all names.

Sensitive.

Namespace

Relative to the root namespace; Global. opts out.

Written in full.

Types without modifiers

Friend.

internal.

Members without modifiers

Public, except Dim fields (Private; Public in a structure).

private.

Line end

The physical line, with implicit continuation.

;.

Block end

End If, Next, Loop, End Sub.

}.

Comment

', REM, '''.

//, /* */, ///.

Imports

Imports with alias, type and XML forms; project-level imports via -imports.

using, using static, global using.

Where the books are outdated

The two reference books for this section were written around the .NET Framework templates. Their program-structure material needs these corrections, stated in the form that works today:

  • Both books start from a Console (.NET Framework) project template. The current template is Console for .NET 10 (dotnet new console -lang vb), the file is Program.vb with Module Program, and Sub Main(args As String()) is the default. A console program does not need .NET Framework.

  • Quickstart Guide pp. 126-127 use Class MyClass, which does not compile because MyClass is reserved (BC30183 in the listing above); write Class [MyClass] or choose a different name. Its pp. 133-134 write Public Class Dog Inherits Animal on one line, which fails with BC30205; put Inherits on the next line, or write Class Dog : Inherits Animal.

  • Quickstart Guide p. 32 mentions "global variables declared outside of any module". No such declaration exists: code and variables live inside a type, and a Public Shared field or a Module field is the closest thing.

  • Mastering VB.NET pp. 161-162 call Async Main, which Visual Basic does not support (BC30737). Use a synchronous Main that waits on the task.

  • The object-oriented project of the Quickstart Guide (pp. 151-161) uses clsGrade-style names (Hungarian prefixes) next to unprefixed ones and does not compile as printed. The conventions above say to leave the prefix out.

  • Learn’s comments page and older sources say that a comment cannot follow a line continuation. That changed with Visual Basic 16.