Option Strict On in VB.NET: An Architecture Note on Strict Typing as the Default
An architecture note on enabling Option Strict On in VB.NET: why project-level strict typing is the smallest suitable design, how it changes data boundaries, and how to verify and migrate safely.
31 Mar 2026, 11:27 UTC

If you maintain a VB.NET codebase that still compiles with Option Strict Off, you are accepting silent type coercion as a design feature. The useful takeaway: enabling Option Strict On at the project level is the smallest design that moves type errors from runtime to compile time, and the migration cost is almost entirely concentrated at data boundaries — file parsing, COM interop, and database reads — which is exactly where you want the compiler watching.
This note assumes VB.NET targeting .NET Framework 4.x or modern .NET (6+), compiled with MSBuild or dotnet build. Option Strict does not exist in classic Visual Basic (VB6/VBA), so nothing here applies to those dialects.
Requirements
The requirement is not "cleaner code." It is a specific operational guarantee: an implicit narrowing conversion — say, a Long silently truncated into an Integer, or a String coerced into a Date using the end user's locale — must fail the build, not corrupt data at 2 a.m. Late binding (calling members on Object variables resolved at runtime) must also fail the build, because a typo in a late-bound member name is a MissingMemberException in production, not a compile error.
A secondary requirement: the rule must apply uniformly. Per-file enforcement lets strictness decay as new files are added without the directive.
The smallest suitable design
The smallest design that satisfies both requirements is one line in the project file, applied to every project in the solution:
<!-- In each .vbproj -->
<PropertyGroup>
<OptionStrict>On</OptionStrict>
</PropertyGroup>This is preferable to scattering Option Strict On at the top of every .vb file. The project-level setting is the default for all files, cannot be forgotten when a file is created, and is visible in one place during code review. A file-level directive still overrides it, which becomes useful for the exceptions discussed below.
Pair it with warnings-as-errors for the conversion-related compiler warnings so nothing slips through as a yellow squiggle nobody reads:
<PropertyGroup>
<OptionStrict>On</OptionStrict>
<WarningsAsErrors>true</WarningsAsErrors>
</PropertyGroup>Run this change in your normal build environment (Visual Studio or dotnet build from a developer command prompt, with restore permissions on the solution). Expect the first build to fail. That failure list is the design working as intended — it is an inventory of every place the code previously relied on coercion.
Trust and data boundaries
Strict typing matters most where data crosses a trust boundary: anything arriving as Object, String, or an untyped COM variant. With Option Strict Off, this compiles and runs:
Function GetDiscount(row As DataRow) As Decimal
Return row("Discount") ' Object -> Decimal, implicit
End FunctionIt works until the column is DBNull or a string, at which point the failure appears far from the boundary. With Option Strict On, the compiler forces the conversion to be explicit and therefore a deliberate decision:
Function GetDiscount(row As DataRow) As Decimal
Dim raw = row("Discount")
If IsDBNull(raw) Then Return 0D
Return Convert.ToDecimal(raw)
End FunctionThe architectural effect is that boundary code becomes honest: every crossing point names its conversion, and the conversion policy (throw, default, clamp) is visible in the source rather than delegated to the runtime's coercion rules.
COM interop is the sharpest boundary. Late binding against COM objects is sometimes unavoidable, and Option Strict On rejects it. The design answer is isolation, not abandonment: wrap the COM surface in a small module with file-level Option Strict Off, expose a strongly typed interface from that module, and keep the rest of the project strict. The boundary file becomes the single audited location for dynamic behavior.
Operational checks
Verification is cheap and should be part of the migration, not an afterthought:
- Build the solution with the setting on. Confirm that a deliberate implicit narrowing (e.g.,
Dim i As Integer = 100000L) produces error BC30512 or an equivalent narrowing-conversion error, and that a late-bound call (Dim o As Object : o.DoThing()) fails with a late-binding error. Exact error numbers vary by compiler version — verify against your toolchain rather than trusting this list. - Fix or explicitly convert each reported site, then confirm a clean build with warnings-as-errors enabled.
- Exercise one boundary function (like
GetDiscountabove) with edge inputs:DBNull, empty string, and an out-of-range number. Confirm the explicit conversion path behaves as designed. - If COM interop exists, confirm the isolated non-strict file compiles and that no other file in the project carries a file-level
Option Strict Offoverride.
Failure modes
The dominant failure mode is behavioral change during migration. An implicit conversion that "worked" may have been quietly truncating or locale-parsing for years; replacing it with CInt or Convert.ToInt32 can change which exceptions are thrown and when. Treat every conversion fix as a behavior review, not a mechanical edit, and cover boundary functions with tests before flipping the switch.
Second: locale-sensitive conversions. Implicit String-to-Date coercion uses ambient culture. Making it explicit with Date.Parse keeps the same risk; prefer Date.TryParseExact with an explicit format and CultureInfo.InvariantCulture where the data format is contractual.
Third: partial adoption. Enabling strictness on one project in a multi-project solution while referencing a non-strict project can leave coercion bugs alive at the seams. Roll out per-project, but track completion across the solution.
Conditions that would change the design
Keep file-level Option Strict Off only where dynamic behavior is the point: COM interop wrappers, reflection-heavy plugin loaders, or code generated against untyped schemas. If a large fraction of the codebase needs the exemption — common in heavy Office-automation code — the project-level default may not be viable yet; isolate the dynamic surface first, then flip the default. If the code is classic VB6 or VBA, this design does not apply at all; the equivalent discipline there is convention and review, not compiler enforcement.
The decision rule is simple: strict by default, dynamic only behind a named boundary, and every conversion at a trust boundary written out where a reviewer can see it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.