Option Statements

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. Four statements, written at the top of a source file or set once for a whole project, decide how strict the compiler is: Option Explicit (must a variable be declared), Option Infer (may the compiler infer a type from an initializer), Option Strict (which conversions and which kinds of late binding are errors) and Option Compare (how strings are compared by default). They are the most consequential settings of the language. A project that compiles with Option Strict Off is silently a different dialect: values change type at run time, a typo in a member name becomes a run-time exception, and a conversion that loses data raises no diagnostic.

The page covers each statement, where its value comes from and which value wins, exactly what Option Strict On rejects (with the real compiler message of each case), the Custom mode that turns the same checks into warnings, the way the .NET SDK silently suppresses those warnings, the effect of Option Compare Text on operators and on runtime-library functions, and a recommended configuration for a solution. Conversions are the subject of Type Conversions; late binding of Object is covered on Early and Late Binding; the type system on Data Types and Literals; and the build properties on Projects, MSBuild and the dotnet CLI.

Every example is a complete program of the shelf library-catalogue scenario or a small project that uses its vocabulary. Each program was built and run on the .NET 10 SDK in a container with Option Strict On, Option Explicit On, Option Infer On and warnings as errors, and the output shown is the real one. Examples that are deliberately wrong are labelled, and the real compiler messages of the build that rejects them are shown instead of run output.

The four statements at a glance

Statement Values SDK project default vbc default Introduced Controls

Option Explicit

On, Off; a bare Option Explicit means On

On

On

Visual Basic before .NET

Whether every variable must be declared with Dim, ReDim or a similar statement before use.

Option Strict

On, Off; a bare Option Strict means On

Off

Off

Visual Basic .NET 2002 (7.0)

Implicit narrowing conversions, late binding and implicit Object typing are errors when On.

Option Infer

On, Off; a bare Option Infer means On

On

On (the vbc.rsp default; the bare compiler internal default is Off)

Visual Basic 9.0 (2008)

Whether a local declaration without As takes the type of its initializer.

Option Compare

Binary, Text

Binary

Binary

Visual Basic before .NET

Default string comparison of =, <>, <, >, Like, Select Case and of the runtime-library functions that have a Compare argument.

The SDK column was read from the real build with dotnet msbuild -getProperty, so it is a fact about SDK 10.0.402 and not only about the documentation. A project with no Option properties at all reports:

dotnet msbuild defs.vbproj -nologo -getProperty:OptionExplicit -getProperty:OptionStrict -getProperty:OptionInfer -getProperty:OptionCompare -getProperty:NoWarn
{
  "Properties": {
    "OptionExplicit": "On",
    "OptionStrict": "Off",
    "OptionInfer": "On",
    "OptionCompare": "Binary",
    "NoWarn": "41999,42016,42017,42018,42019,42020,42021,42022,42032,42036"
  }
}

Source: -optionstrict

Two things in that output deserve attention before anything else. OptionStrict is Off by default: a project created with dotnet new console -lang vb compiles code that every other mainstream typed language would reject, and the section recommends turning it on in every project. And NoWarn is pre-filled with ten warning numbers; they are the warnings that the Custom mode of Option Strict would report, which is why that mode does nothing in an SDK project until the list is replaced (see Custom mode and the SDK NoWarn trap).

Where a setting comes from

A setting is resolved from three places, and the most specific one wins:

  1. The file. An Option statement at the top of a source file applies to that file only. It overrides everything below it.

  2. The project. An MSBuild property (OptionStrict, OptionExplicit, OptionInfer, OptionCompare) is passed to the compiler as /optionstrict, /optionexplicit, /optioninfer and /optioncompare for every file that has no statement of its own. In Visual Studio the same values appear on the project’s Compile page. Putting the properties in a Directory.Build.props file gives the whole directory tree the same value.

  3. The compiler default, which on vbc is -optionstrict-, -optionexplicit+, -optioncompare:binary and, through the response file shipped with the compiler, -optioninfer+. Visual Studio’s Tools > Options > Projects and Solutions > VB Defaults page supplies the initial values for new projects (documented as Explicit On, Strict Off, Compare Binary and Infer On).

The Option statements of a file are subject to three placement rules, each enforced by the compiler:

  • They must appear before any other statement of the file: before Imports, attributes and declarations. The order among the Option statements themselves is free, and conventionally Option Strict On comes first.

  • Each of the four statements may appear at most once per file.

  • They have file scope. There is no way to switch a setting on for a part of a file, and Option statements cannot be placed in a Module, Class or procedure.

The compiler reports the violations like this (a file with a doubled Option Strict, a doubled Option Compare, and, in a second build, an Option Explicit placed inside a module):

Program.vb(2,1): error BC30225: 'Option Strict' statement can only appear once per file.
Program.vb(4,1): error BC30225: 'Option Compare' statement can only appear once per file.
Program.vb(7,5): error BC30627: 'Option' statements must precede any declarations or 'Imports' statements.

Because the file wins over the project, the settings of an individual file can be tightened or relaxed without touching the project. The next program shows it for Option Compare, the one statement that has a legitimate reason to differ between files. The project sets <OptionCompare>Text</OptionCompare>; Program.vb has no statement and follows the project, and Exact.vb declares Option Compare Binary and overrides it.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <OptionStrict>On</OptionStrict>
    <OptionCompare>Text</OptionCompare>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>
Module Program
    Sub Main()
        Dim stored As String = "Dune"
        Dim typed As String = "DUNE"
        Console.WriteLine("Program.vb, no Option Compare statement : " & (stored = typed).ToString())
        Console.WriteLine("Exact.vb, Option Compare Binary         : " & Exact.SameTitle(stored, typed).ToString())
    End Sub
End Module
Option Compare Binary

Module Exact
    Function SameTitle(left As String, right As String) As Boolean
        Return left = right
    End Function
End Module
Program.vb, no Option Compare statement : True
Exact.vb, Option Compare Binary         : False

Program.vb in that listing has no Option Strict On line because the project property supplies it; that is the style recommended for a solution (see the recommended configuration), and it is the one place where a missing Option Strict On is not a defect. A reader of a file alone cannot see the project property, which is the argument for repeating Option Strict On at the top of every file of a library that is published as source.

Option Explicit

Option Explicit On makes the use of an undeclared name a compile-time error. It is on by default in the SDK, in vbc and in Visual Studio, and it is what makes a misspelled variable name fail to compile instead of creating a new variable. The declaration statements it refers to are Dim, ReDim, Static, Const, For/For Each control variables, Using, Catch and the like (the declaration statements themselves are explained on Variables, Constants and Scope).

With Option Explicit Off, using a name that is not declared creates a local variable of type Object on the spot. The compiler accepts a typo, and on Option Strict Off nothing flags it either. The next file combines Option Explicit Off with Option Strict On, which is the only combination that is not completely silent: Option Strict On rejects what is done with the implicitly declared variables, because they are Object. The program is deliberately wrong; it is built to show the diagnostics.

Option Strict On
Option Explicit Off

Module Program
    Function CountCopies(loans As Integer(), onShelf As Integer) As Integer
        copies = onShelf
        For Each loaned As Integer In loans
            copes = copies - loaned
        Next
        Return copies
    End Function

    Sub Main()
        Console.WriteLine(CountCopies(New Integer() {1, 2}, 10))
    End Sub
End Module
Program.vb(8,21): error BC30038: Option Strict On prohibits operands of type Object for operator '-'.
Program.vb(10,16): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Integer'.

Line 6 declares copies implicitly and line 8 misspells it as copes. Under Option Strict On both uses are flagged because the implicit variables are Object (BC30038 for the subtraction, BC30512 for the Return). With Option Strict Off as well, the same file builds, and CountCopies returns the wrong value or throws at run time depending on what the variables hold. A file that only assigns and prints an implicitly declared variable, such as total = 5 followed by Console.WriteLine(total), also builds under Option Strict On on SDK 10.0.402. Option Explicit Off is therefore not made safe by Option Strict On.

Facts worth knowing about Option Explicit:

  • Learn documents the page’s own recommendation: Option Explicit Off is "generally not a good practice". There is no scenario in new code that needs it. It exists for source compatibility with Visual Basic 6, where implicit declaration was the default.

  • The Option Explicit statement overrides the -optionexplicit compiler option and the OptionExplicit MSBuild property for its file, in the same way as the other three.

  • The correct fix for BC30451, "'name' is not declared. It may be inaccessible due to its protection level", is not always a Dim: the name may be misspelled, out of scope, or declared Private in another type. The message lists the protection level because it is the second most common cause.

  • A C# program has no counterpart: C# always requires a declaration, and its var is the counterpart of Option Infer On, not of implicit declaration.

Option Infer

Option Infer On allows a local variable declared without an As clause to take the type of its initializer. It was added in Visual Basic 9.0 together with LINQ, whose anonymous types and query variables need it, and it applies to local variables of procedures only: not to fields, properties, parameters or function return types.

The combination of Dim, a type and an initializer is the following (the results mirror Learn’s table; the Strict On column was checked with the compiler):

Declaration Option Infer Option Strict Off Option Strict On

Dim qty = 5

On

Integer

Integer

Dim qty = 5

Off

Object

Error BC30209 (requires an As clause)

Dim qty

any

Object holding Nothing

Error BC30209

Dim qty = Nothing

On

Object holding Nothing

Object, allowed: the documented exception

Dim qty As Integer

any

Integer, initialised to 0

Integer, initialised to 0

The exception on the fourth row is worth remembering: Dim something = Nothing is the one declaration without As that Option Strict On accepts, and the variable it creates is an Object. Write Dim something As Book = Nothing instead. The type of the variable is the type of the initializer, not what the program later assigns to it, and a numeric literal is as narrow as its form says: Dim copies = 3 is an Integer, Dim big = 3000000000 is a Long, Dim price = 19.99 is a Double and Dim price = 19.99D is a Decimal. The literal rules are on Data Types and Literals.

Turning Option Infer off on top of Option Strict On makes Dim without As an error (BC30209) and also rejects the For Each item In collection form, because the control variable’s type is inferred too (the compiler reports the loop variable as undeclared, BC30451). There is no benefit in Option Infer Off for new code. It can be useful for one purpose: to find every declaration without a type in code that was written without caution, by turning it off and reading the errors.

Option Strict

Option Strict On has three effects, which Learn lists as the three conditions that cause a compile-time error. They correspond to the three warning settings of the project designer’s Compile page:

  1. Implicit narrowing conversions are errors. A conversion that can lose data or fail at run time (Double to Integer, Long to Integer, String to a number, Object to anything, a base type to a derived type) must be written out with a conversion function or CType/DirectCast/TryCast.

  2. Late binding is an error. A member access on an expression of type Object is resolved at run time only with Option Strict Off; with On, it does not compile.

  3. Implicit Object typing is an error. A declaration, a function result, a parameter or a property without an As clause is an error, except for the Dim x = Nothing case above.

Two further groups of rules are consequences of the first. Operators do not accept Object operands (=, + and the others raise BC32013 or BC30038), and a number is never converted to Boolean, String or Date implicitly. The widening direction is unrestricted: Integer to Long, Double or Decimal, Char to String, a derived type to its base, anything to Object, an enumeration to its underlying type, and a constant to any numeric type that can hold its value.

What the compiler says

The next program collects the implicit conversions and the late binding that Option Strict On rejects. Every line that is marked in the build output is a separate error, and the conversion-function replacement for each is on Type Conversions. The program is deliberately wrong and was built to show the diagnostics; it has no run output.

Option Strict On

Module Program
    Sub Main()
        Dim price As Double = 19.99
        Dim whole As Integer = price
        Dim anything As Object = "text"
        Dim count As Integer = anything
        Console.WriteLine(anything.Length)
        Dim untyped
        Dim flag As Boolean = "True"
        Dim published As Date = "2026-10-19"
        Dim label As String = 42
        Dim initial As Char = "ab"
        Dim average As Integer = 10 / 4
        Dim cube As Integer = 2 ^ 3
    End Sub
End Module
Program.vb(10,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Program.vb(10,13): warning BC42024: Unused local variable: 'untyped'.
Program.vb(11,31): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Boolean'.
Program.vb(12,33): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Date'.
Program.vb(13,31): error BC30512: Option Strict On disallows implicit conversions from 'Integer' to 'String'.
Program.vb(14,31): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Char'.
Program.vb(15,34): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(16,31): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(6,32): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(8,32): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Integer'.
Program.vb(9,27): error BC30574: Option Strict On disallows late binding.

The unused untyped variable also draws warning BC42024, a diagnostic that exists independently of the Option statements. The messages are listed in the compiler’s order, which is not the order of the lines. Notice what the output says about two lines that look harmless: Dim average As Integer = 10 / 4 and Dim cube As Integer = 2 ^ 3 are rejected because / and ^ always return a Double, so the result of dividing two Integer values needs \ (integer division) or CInt. The book errata list in the section outline contains exactly this mistake, so the correct form is stated here: an Integer quotient is written with the \ operator or an explicit CInt(a / b), and the exponentiation operator is followed by a conversion.

Declarations are checked before bodies, so a missing As on a parameter or a function stops the build earlier and hides the other errors. This one-function file builds on its own:

Option Strict On

Module Declarations
    Function Echo(value)
        Return value
    End Function
End Module
Declarations.vb(4,14): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.
Declarations.vb(4,19): error BC30211: Option Strict On requires that all method parameters have an 'As' clause.

The second group is what the operators and the control statements reject. It was built from the following program, which is deliberately wrong as well:

Option Strict On

Module Program
    Sub Take(ByRef n As Integer)
        n += 1
    End Sub

    Sub Main()
        Dim a As Object = 5
        Dim b As Object = 6
        Console.WriteLine(a = b)
        Console.WriteLine(a + b)
        If a Then Console.WriteLine("truthy")
        Dim something = Nothing
        Console.WriteLine(something Is Nothing)
        Dim total As Long = 10
        Take(total)
        For Each i As Integer In New Long() {1L, 2L}
            Console.WriteLine(i)
        Next
        For i As Integer = 1 To 3.5
            Console.WriteLine(i)
        Next
        Dim flag As Boolean = 1
        Select Case a
            Case 5
                Console.WriteLine("five")
        End Select
        Dim items As Object() = {1, 2}
        Dim n As Integer = items(0)
    End Sub
End Module
Program.vb(11,27): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(11,31): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(12,27): error BC30038: Option Strict On prohibits operands of type Object for operator '+'.
Program.vb(12,31): error BC30038: Option Strict On prohibits operands of type Object for operator '+'.
Program.vb(13,12): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Boolean'.
Program.vb(17,14): error BC30512: Option Strict On disallows implicit conversions from 'Long' to 'Integer'.
Program.vb(21,33): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(24,31): error BC30512: Option Strict On disallows implicit conversions from 'Integer' to 'Boolean'.
Program.vb(25,21): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(30,28): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'Integer'.

Read the output against the source, because three lines compile although they look similar to rejected ones:

  • Line 14, Dim something = Nothing, builds (the exception from the table).

  • Line 18, For Each i As Integer In New Long() {1L, 2L}, builds. Learn documents that narrowing conversions from the elements of a For Each collection to the loop variable are not checked, even with Option Strict On. The conversion happens at run time and throws OverflowException if a Long element does not fit. Prefer the loop variable of the element’s own type.

  • Line 17, Take(total), is an error although Take takes ByRef: a ByRef argument must be of exactly the parameter’s type, or the conversion in either direction would have to be checked.

Line 12, a + b over two Object values, is BC30038 and not BC32013, which is the one for =. The message of BC32013 contains the useful hint: to test whether two Object expressions are the same instance, use Is. A comparison of values needs a conversion first: CInt(a) = CInt(b) or, for a type check, TypeOf a Is Integer.

The error catalogue

Code Message (abridged) Typical cause and fix

BC30512

Option Strict On disallows implicit conversions from 'X' to 'Y'.

A narrowing conversion, or an unrelated pair such as String and Date. Use CInt, CDbl, CType, Convert, Integer.Parse/TryParse or a cast that expresses the intent.

BC30574

Option Strict On disallows late binding.

A member was looked up on an Object. Declare the variable with its real type, or DirectCast/TryCast it first.

BC30209

Option Strict On requires all variable declarations to have an 'As' clause.

Dim x without an initializer, or Dim x = y with Option Infer Off.

BC30210

Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.

A function or property without its return type.

BC30211

Option Strict On requires that all method parameters have an 'As' clause.

A parameter without its type.

BC30038

Option Strict On prohibits operands of type Object for operator 'x'.

Arithmetic or concatenation over an Object. Declare typed variables, or convert first.

BC32013

Option Strict On disallows operands of type Object for operator '='.

The comparison operators over two Object operands. Compare with Is, or convert.

BC42016

Implicit conversion from 'X' to 'Y'.

The warning counterpart of BC30512 in Custom mode.

BC42017

Late bound resolution; runtime errors could occur.

The warning counterpart of BC30574.

BC42020

Variable declaration without an 'As' clause; type of Object assumed.

The warning counterpart of BC30209.

The error-message index of the documentation does not describe every one of these numbers (BC30512, BC30574 and BC30209 are described in the compiler output and in the Option Strict article, not on a page of their own), so the table above is built from the compiler’s messages as printed by SDK 10.0.402.

What Option Strict Off does instead

Rewriting a legacy project for Option Strict On is easier when you know what each error replaces. The following behaviour was observed on SDK 10.0.402 in a throwaway project with a file-level Option Strict Off; it is listed here as a description of what the conversions do, and the page deliberately contains no listing of that code.

Statement under Option Strict Off What happens

Dim x As Integer = 3.7

Compiles; x is 4. The conversion is CInt, which rounds to even.

Dim n As Integer = "12"

Compiles; n is 12. A non-numeric string throws InvalidCastException at run time.

Dim flag As Boolean = "True"

Compiles and parses the string.

Dim d As Date = "2026-10-19"

Compiles and parses the string with the current culture.

Dim text As String = 7

Compiles; the text is 7.

Dim s = "5" then s + 1

Is the Double 6: with a String operand the + operator converts to a number. With & the result would be the string 51.

Dim o As Object = 3 then o + 4

Is the Integer 7, decided at run time.

Dim late As Object = "hello" then late.Lenght

Compiles; throws MissingMemberException: "Public member 'Lenght' on type 'String' not found."

Dim small As Integer = bigLong with a value above Integer.MaxValue

Compiles; throws OverflowException at run time.

Each of these is a run-time failure or a silent change of meaning that Option Strict On turns into a compile-time message. The rounding of the first row is the banker’s rounding of the conversion functions, covered on Type Conversions.

Widening, inference and the constants

What Option Strict On still accepts is the reason it is practical. The following program is complete and warning-free. It shows the inferred types, the widening conversions that stay implicit, the rule for constants, the For Each exception and the operators that return a fixed type:

Option Strict On
Option Infer On

Imports System.Collections.Generic

Module Program
    Sub Main()
        ' Option Infer On: the type comes from the initializer.
        Dim title = "Dune"
        Dim copies = 3
        Dim price = 19.99D
        Dim ratio = copies / 2
        Dim shelf = New List(Of String) From {"Dune", "Emma"}
        Dim nothingYet = Nothing
        Console.WriteLine(TypeName(title) & ", " & TypeName(copies) & ", " & TypeName(price))
        Console.WriteLine(TypeName(ratio) & ", " & TypeName(shelf) & ", " & TypeName(nothingYet))

        ' Widening conversions are implicit and allowed under Option Strict On.
        Dim small As Short = 12
        Dim wide As Long = copies
        Dim real As Double = wide
        Dim money As Decimal = copies
        Dim letter As Char = "A"c
        Dim text As String = letter
        Dim boxed As Object = text
        Console.WriteLine(small.ToString() & " " & wide.ToString() & " " & real.ToString() & " " & money.ToString() & " " & text & " " & TypeName(boxed))

        ' A constant that fits needs no cast, a run-time value does.
        Dim byteValue As Byte = 200
        Dim fromRuntime As Integer = copies * 100
        Dim narrowed As Byte = CByte(fromRuntime Mod 256)
        Console.WriteLine(byteValue.ToString() & " " & narrowed.ToString())

        ' For Each is the documented exception: element conversions are not checked.
        For Each n As Integer In New Long() {1L, 2L}
            Console.Write(n.ToString() & " ")
        Next
        Console.WriteLine()

        ' Integer division and Mod keep integers, / and ^ always return Double.
        Console.WriteLine((7 \ 2).ToString() & " " & (7 Mod 2).ToString() & " " & (7 / 2).ToString() & " " & (2 ^ 3).ToString())
        Console.WriteLine(TypeName(7 \ 2) & " " & TypeName(7 / 2) & " " & TypeName(2 ^ 3))
    End Sub
End Module
String, Integer, Decimal
Double, List(Of String), Nothing
12 3 3 3 A String
200 44
1 2
3 1 3.5 8
Integer Double Double

The line Dim byteValue As Byte = 200 compiles because 200 is a constant that fits a Byte; Dim byteValue As Byte = 300 is BC30439 ("Constant expression not representable in type 'Byte'") under every setting. The run-time value on the next line has to be narrowed explicitly, and CByte(fromRuntime Mod 256) makes the intent visible and the range impossible to exceed. Also notice that 7 / 2 is 3.5 and 7 \ 2 is 3: Option Strict On never changes what an operator returns, only what may be assigned to what.

Custom mode and the SDK NoWarn trap

Option Strict has a third state, Custom, in which the compiler applies the Strict-On analysis but reports each finding as a warning instead of an error. It is what Visual Studio shows on the Compile page when the three warning configurations are not all Error and not all None, and it is what the -optionstrict:custom switch of vbc selects. It is the right tool for a migration: turn it on, then move warnings to errors one category at a time. The ten warnings are BC41999, BC42016 to BC42022, BC42032 and BC42036 (implicit conversion, late bound resolution, implicit Object types and the related cases).

An SDK-style project has two traps that the documentation does not mention:

  • The MSBuild property OptionStrict is a Boolean for the compiler task. Writing <OptionStrict>Custom</OptionStrict> fails the build with error MSB4030: "Custom" is an invalid value for the "OptionStrict" parameter of the "Vbc" task. Custom mode is selected with the separate property OptionStrictType: <OptionStrictType>Custom</OptionStrictType>.

  • The SDK’s props file defines NoWarn as 41999,42016,42017,42018,42019,42020,42021,42022,42032,42036 when the project has not set it. Those are exactly the warnings of Custom mode, so a project that sets OptionStrictType to Custom and nothing else builds with no diagnostics at all. Any non-empty NoWarn value of your own replaces the default, and a project that wants Custom mode has to set one (the number is arbitrary; 42353 is a warning number that this project never produces).

The following project does both and escalates two of the ten warnings. The source file has no Option statements, so the project properties decide:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>Ex5</RootNamespace>
    <OptionStrictType>Custom</OptionStrictType>
    <!-- The SDK suppresses the Option Strict warnings by default; any non-empty NoWarn replaces that list. -->
    <NoWarn>42353</NoWarn>
    <!-- Escalate two of the ten: implicit conversion and late binding. -->
    <WarningsAsErrors>$(WarningsAsErrors);42016;42017</WarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
</Project>
Module Program
    Sub Main()
        Dim price As Double = 19.99
        Dim whole As Integer = price
        Dim anything As Object = "text"
        Console.WriteLine(anything.Length)
        Dim untyped
        Console.WriteLine(whole)
    End Sub
End Module
Program.vb(4,32): error BC42016: Implicit conversion from 'Double' to 'Integer'.
Program.vb(6,27): error BC42017: Late bound resolution; runtime errors could occur.
Program.vb(7,13): warning BC42020: Variable declaration without an 'As' clause; type of Object assumed.
Program.vb(7,13): warning BC42024: Unused local variable: 'untyped'.

Source: -warnaserror

BC42016 and BC42017 are errors because they are in WarningsAsErrors; BC42020 and BC42024 stay warnings. The build fails, so there is no run output, and the example shows that the same findings that Option Strict On reports as BC30512 and BC30574 appear here under the warning numbers. A warning number can also be moved in a .editorconfig, which is more convenient when the project has many files and a few exceptions. With OptionStrictType set to Custom and NoWarn replaced as above, this file made BC42016 an error and silenced BC42020, while BC42017 stayed a warning:

root = true

[*.vb]
dotnet_diagnostic.BC42016.severity = error
dotnet_diagnostic.BC42020.severity = none

Source: -nowarn

The command-line forms of the same switches are -optionstrict+, -optionstrict-, -optionstrict:custom, -nowarn:42016,42017 and -warnaserror:42016,42017. The Visual Studio walkthrough is Learn’s Configuring warnings in Visual Basic. For a project that is already at Option Strict On, none of this is needed: Custom mode is a path towards On, not a place to stay.

Option Compare

Option Compare selects the default method used when two strings are compared:

  • Binary compares the numeric values of the characters (UTF-16 code units, in practice). The comparison is case-sensitive and culture-independent, fast, and gives the order A < B < Z < a < b < z.

  • Text compares by the lexical meaning of the characters in the current culture, ignoring case, kana type and character width. It is slower and gives the order A = a < B = b < Z = z (Learn’s example adds the accented letters of code page 1252).

Learn’s description of Text is "determined by your system’s locale", so a program that depends on the culture-specific part can behave differently on two machines. The case-insensitive part is reliable.

The setting affects:

  • The comparison operators =, <>, <, <=, >, >= over String operands.

  • Like, whose literal characters and ranges are compared by the same method ("abc" Like "A*" is True under Text and False under Binary; the Like operator is supported on .NET, contrary to a stale note on its Learn page).

  • Select Case over strings.

  • The runtime-library functions that have an optional Compare parameter: InStr, InStrRev, StrComp, Replace, Split, Filter. The compiler passes the file’s setting for that argument, which means that the default value shown in the documentation is not what the function receives (see the next listing: Replace defaults to Binary in its signature, but on a Text file it replaces both cases).

The setting does not affect anything in the .NET Base Class Library:

  • String.Equals, String.Compare, String.Contains, String.StartsWith and the ==-like methods follow their own rules (StringComparison), which default to ordinal for Equals and Contains.

  • Collections and hashes: Dictionary(Of String, T) uses the default comparer (ordinal) unless one is given. It is the most common surprise: "Shelf" = "SHELF" is True in the file but ContainsKey("SHELF") on a dictionary keyed by "Shelf" is False.

  • LINQ methods such as Distinct, Contains and GroupBy over strings use the comparer of the method (ordinal by default), not the file’s option. A Where t = "DUNE" inside a query expression is different: there = is the Visual Basic operator, so it follows Option Compare. The ordering of Order By is culture-dependent and is not controlled by the option either.

The next program shows the whole matrix on one file with Option Compare Text:

Option Strict On
Option Compare Text

Imports System.Collections.Generic
Imports System.Linq
Imports Microsoft.VisualBasic

Module Program
    Sub Main()
        Dim stored As String = "Shelf"
        Dim typed As String = "SHELF"

        ' Operators and Select Case follow the file's Option Compare setting.
        Console.WriteLine("stored = typed          : " & (stored = typed).ToString())
        Console.WriteLine("stored <> typed         : " & (stored <> typed).ToString())
        Console.WriteLine("abc Like A*             : " & ("abc" Like "A*").ToString())
        Select Case typed
            Case "shelf"
                Console.WriteLine("Select Case             : matched")
            Case Else
                Console.WriteLine("Select Case             : missed")
        End Select

        ' Runtime-library functions with an optional Compare argument take the file's default.
        Console.WriteLine("InStr default           : " & InStr("Library Shelf", "shelf").ToString())
        Console.WriteLine("InStr Binary            : " & InStr(1, "Library Shelf", "shelf", CompareMethod.Binary).ToString())
        Console.WriteLine("StrComp default         : " & StrComp(stored, typed).ToString())
        Console.WriteLine("StrComp Binary          : " & StrComp(stored, typed, CompareMethod.Binary).ToString())
        Console.WriteLine("Replace default         : " & Replace("Shelf SHELF", "shelf", "x"))
        Console.WriteLine("Split default parts     : " & Split("a-B-c", "b").Length.ToString())

        ' The .NET Base Class Library ignores Option Compare completely.
        Console.WriteLine("String.Equals           : " & String.Equals(stored, typed).ToString())
        Console.WriteLine("stored.Contains(HEL)    : " & stored.Contains("HEL").ToString())
        Dim keys As New Dictionary(Of String, Integer)()
        keys("Shelf") = 1
        Console.WriteLine("Dictionary.ContainsKey  : " & keys.ContainsKey("SHELF").ToString())

        ' In a query expression, = is the VB operator; Distinct uses the .NET default comparer.
        Dim titles As String() = {"dune", "Dune", "Emma", "emma"}
        Dim matches = From t In titles Where t = "DUNE" Select t
        Console.WriteLine("Query Where t = DUNE    : " & matches.Count().ToString())
        Console.WriteLine("Distinct count          : " & titles.Distinct().Count().ToString())
    End Sub
End Module
stored = typed          : True
stored <> typed         : False
abc Like A*             : True
Select Case             : matched
InStr default           : 9
InStr Binary            : 0
StrComp default         : 0
StrComp Binary          : 1
Replace default         : x x
Split default parts     : 2
String.Equals           : False
stored.Contains(HEL)    : False
Dictionary.ContainsKey  : False
Query Where t = DUNE    : 2
Distinct count          : 4

Source: Strings.InStr

InStr("Library Shelf", "shelf") is 9 because the match ignores case; the explicit CompareMethod.Binary argument in the next line gives 0. StrComp returns 0, -1 or 1 and follows the same rule. The last three lines are the .NET side: the dictionary, Equals and Contains are ordinal whatever the file says.

Guidance on the choice:

  • Use Binary (the default) everywhere you can. It is predictable, matches C#, and is the best performing option. For identifiers, keys, ISBNs, file names and protocol text the ordinal comparison is the correct one. Case-insensitive logic belongs at the call site, where it is visible: String.Equals(a, b, StringComparison.OrdinalIgnoreCase) for identifiers, or StringComparer.CurrentCultureIgnoreCase for user-facing sorting.

  • Use Option Compare Text only in a file whose whole purpose is case-insensitive text matching with the VB operators (a small command-parsing module, for example), and say so in a comment. It is a file-level setting precisely so that it stays local.

  • Never set Text for a whole project. Code in other files then compares differently from what its author expects, and a reader cannot see why.

  • In a library meant for others, state Option Compare Binary explicitly at the top of the file if the logic depends on it, so that a project-level OptionCompare property cannot change it.

For a new solution, set the four values once in a Directory.Build.props file at the repository root. This is the file the shelf solution uses (it builds all of its projects, including the library, the console application and the tests, warning-free):

<Project>
  <PropertyGroup>
    <OptionExplicit>On</OptionExplicit>
    <OptionStrict>On</OptionStrict>
    <OptionInfer>On</OptionInfer>
    <OptionCompare>Binary</OptionCompare>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

Source: -optionstrict

Notes on the choice:

  • Explicit On and Infer On are already the SDK defaults, and are written out so that the file documents the intent and survives a change of defaults. Strict On is the one that changes behaviour.

  • Infer On is not a loophole of Strict On; it is what lets Dim books = catalog.Search(term) stay readable. The type is still static and checked.

  • In an existing project, the migration path is OptionStrictType Custom with the SDK NoWarn replaced, escalating one category of warning at a time. Late binding (BC42017) is the category that needs the most thought, because it points at Office automation, COM and reflection code where a typed alternative may not exist. For such a file, the file-level statement is the supported escape hatch, and the page Early and Late Binding shows its single legitimate use.

  • The .editorconfig severities (dotnet_diagnostic.BCnnnnn.severity) complement the options and do not replace them.

  • Generated files such as designer files carry their own Option statements and are not affected by the properties of the project for the settings they declare.

  • The Visual Studio VB Defaults page changes only the initial values of new projects. Changing them in the IDE does not change an existing project; the property in the project file does.

Differences from C#

Concern Visual Basic C#

Undeclared variable

Error with Option Explicit On; implicit Object local with Off.

Always an error.

Implicit narrowing

Error with Option Strict On; silent conversion with Off.

Always an error; checked/unchecked decides overflow.

Late binding

Allowed with Option Strict Off on any Object.

Only with the explicit dynamic type.

Type inference

Option Infer On and a declaration without As.

var, always available.

String comparison

= follows Option Compare.

== is ordinal; comparison types are explicit.

Where it is set

File statement, project property, vbc switch.

Language version, nullable and analyzer severities; no equivalent of the four options.

The practical conclusion for a team that uses both languages is that Visual Basic with Option Strict On is the same language as C# for the purposes of conversions and type safety; the rest of the section assumes that setting.

Pitfalls

Pitfall What to do

A project created from the SDK template has Option Strict Off.

Set the properties in Directory.Build.props the day the solution is created. A project with Off accumulates conversions that are expensive to fix later.

Option Strict On in a file but not in the project: one file is safe, the others are not.

Grep for files without the statement, or set the property. A Directory.Build.props plus the Option Strict On line on every file is the redundant-but-safe pair.

OptionStrict set to Custom fails with MSB4030.

Use OptionStrictType.

Custom mode builds with no warnings at all.

The SDK NoWarn default hides them. Replace it.

A Text comparison in the file and an ordinal dictionary in the code.

The option affects operators and VB functions only. Give the dictionary a comparer.

A culture-dependent Text result in tests.

Tests that run on a machine with another culture may order or compare differently. Use Binary, or an explicit culture.

The For Each loop variable of a narrower type than the elements.

Not checked at compile time. Use For Each x In sequence with inference, or convert explicitly inside the loop.

Option Explicit Off with Option Strict On still compiles a bare assignment.

Keep Explicit on. It costs nothing.

Where the books are silent or wrong

Neither of the two reference books for this section mentions Option Strict: they teach Option Explicit as a historical Visual Basic 6 setting and mix declared and implicit variables. Quickstart Guide p. 31 labels "implicit declaration" what is really type inference through Option Infer, and its Option Explicit example is wrong in both dialects. The correct reading is the one on this page: Option Explicit is about declaring names, Option Infer is about typing a declared name from its initializer, and Option Strict is about what may be converted and bound at run time. Mastering VB.NET p. 43 assigns the result of / and of ^ to an Integer variable, which is an error under Option Strict On because both operators return Double; the correct forms are the \ operator or an explicit conversion, as shown above. Both books were written for a toolchain whose templates compile such code, and the SDK’s own Option Strict Off default is the reason it still compiles in a new project today.