Object Lifetime and Disposal

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors (except the one that is meant to fail) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The project’s root namespace is App, which is why a type name in an exception message reads App.LoggingScannerSession.

Visual Basic objects live on the .NET garbage-collected heap, so the language itself has very little to say about memory: there is no delete, Set x = Nothing is no longer a cleanup ritual, and nothing is reference-counted. What remains is the part the collector cannot do: releasing non-memory resources (file handles, sockets, database connections, native handles) at a predictable moment. That is the job of IDisposable, the Using statement and, as a safety net, the finalizer. This page shows how Visual Basic spells each of them, where the language differs from C# and which traps are specific to VB. The runtime mechanics (generations, the large object heap, server and workstation modes, SafeHandle internals, pooling and diagnostics) are the same for every .NET language and are described once, in C#: Memory Management and Disposal.

How objects are created and destroyed

An object is created by New, which allocates it on the managed heap, runs the field initializers and then the constructor (Sub New). It stays alive for as long as it is reachable: referenced from a static field, from a local variable of a method that is still running (and the JIT may consider a local dead before its scope ends), from another reachable object, or from a GC handle. When nothing reachable refers to it, it becomes garbage; the collector reclaims its memory at some later point that you do not control and cannot observe from the language.

flowchart LR A[New T] --> B[Field initializers and Sub New] B --> C{Reachable?} C -->|yes| C C -->|no| D{Has Finalize?} D -->|no| F[Memory reclaimed] D -->|yes| E[Queued for the finalizer thread] E --> G[Finalize runs] G --> F B -.-> H[Dispose called by the owner] H -.-> I[GC.SuppressFinalize skips the finalizer queue]

Two different things can therefore happen at the end of an object’s life, and they must not be confused.

Concern Memory Other resources (handle, connection, lock)

Who releases it

the garbage collector

the owner of the object, through Dispose

When

at an unspecified time after the object is unreachable

at the end of a Using block, or when the owner decides

Visual Basic syntax

nothing: just stop referencing the object

Using ... End Using, or an explicit Dispose() call

Safety net

not needed

a finalizer (Finalize), only for types that own an unmanaged resource directly

The older VB6 Class_Terminate event, which ran immediately when the last reference disappeared, has no equivalent: .NET has no reference counting. Code that was written for VB6 and relied on Set obj = Nothing to close a file, release a COM server or run Class_Terminate has to be rewritten with Using (see Migrating Legacy Visual Basic). Class_Initialize became Sub New, which is described in Classes and Objects.

Initialisation order and the virtual-call trap

The order in which a derived object is built is fixed, and it is the reverse of the order in which a C# programmer expects the fields to be initialised. In Visual Basic the field initializers of a class run at the start of its own constructor, after the call to MyBase.New has returned. In C# the field initializers of a class run before the base constructor call. The consequence is that a virtual method called from a base constructor sees a derived object whose fields are still Nothing or zero, in both languages, but in Visual Basic the derived field initializers have not run either:

Option Strict On

Public Class Base
    Private ReadOnly _field As Integer = Trace("Base field initializer")

    Public Sub New()
        Console.WriteLine("Base.New")
        Describe()
    End Sub

    Protected Overridable Sub Describe()
        Console.WriteLine("Base.Describe")
    End Sub

    Protected Shared Function Trace(text As String) As Integer
        Console.WriteLine(text)
        Return 1
    End Function
End Class

Public Class Derived
    Inherits Base

    Private ReadOnly _label As String = Describe2()

    Public Sub New()
        MyBase.New()
        Console.WriteLine("Derived.New, label = " & _label)
    End Sub

    Private Shared Function Describe2() As String
        Console.WriteLine("Derived field initializer")
        Return "set"
    End Function

    Protected Overrides Sub Describe()
        Console.WriteLine("Derived.Describe, label = " & If(_label, "<Nothing>"))
    End Sub
End Class

Module Program
    Sub Main()
        Dim d As New Derived()
    End Sub
End Module
Base field initializer
Base.New
Derived.Describe, label = <Nothing>
Derived field initializer
Derived.New, label = set

Derived.Describe ran while the object was only half built: _label was still Nothing. Never call an overridable member (Overridable, MustOverride, Overrides) from a constructor; the compiler does not warn about it unless a code-analysis rule such as CA2214 is enabled (see XML Documentation, Analyzers and Code Style). For the other half of the story, constructors that throw, see A constructor that throws.

Garbage collection in brief

The collector is generational, tracing and compacting, and it runs on its own schedule: when a generation’s allocation budget is exhausted, when the machine is short of memory, or when you call GC.Collect. What matters for Visual Basic code is a short list of consequences, all explained with the supporting numbers in the C# page.

  • Allocation is cheap and short-lived objects are cheaper still. Do not pool or cache small objects "to help the collector".

  • You cannot predict when an object is collected, and you cannot rely on a finalizer having run at any particular moment.

  • A local variable does not keep an object alive after its last use in optimised code, whatever its declared scope. GC.KeepAlive(obj) marks a point up to which the object must stay reachable.

  • GC.Collect() is for tests and diagnostics; calling it in production code is almost never justified.

The members you will meet are GC.Collect, GC.WaitForPendingFinalizers, GC.GetGeneration, GC.CollectionCount, GC.GetTotalMemory, GC.SuppressFinalize, GC.ReRegisterForFinalize and GC.KeepAlive. They are ordinary static methods of System.GC and need no Visual Basic-specific syntax.

The Using statement

Using guarantees that Dispose is called on a resource when control leaves the block, by any route: falling off the end, Return, Exit Sub/Exit Function, GoTo, or an exception. It has been part of Visual Basic since Visual Basic 2005 (8.0). The compiler expands it into a Try ... Finally that calls Dispose if the resource is not Nothing.

Using resource1 [, resource2 ...]
    ' statements
End Using

resource: name [As [New] Type] = expression    ' declared, named resource
          name As New Type(arguments)           ' declared, constructed in place
          expression                            ' an existing variable or expression (the "expression form")
  • The resource type must implement System.IDisposable; otherwise the compiler reports BC36010 (shown below).

  • A declared resource variable is scoped to the block and is read-only inside it: assigning to it is BC30064.

  • Several resources can be listed in one statement, separated by commas. They are disposed in the reverse order of declaration, exactly as nested Using blocks would be.

  • A resource may be Nothing; the Finally skips the Dispose call. This is why Using file As FileStream = TryOpen() is safe.

  • In the expression form the variable is declared elsewhere and keeps existing after the block, but it has been disposed. The compiler captures the value of the expression when the block starts, so reassigning the variable inside the block does not change what is disposed.

  • There is no Exit Using statement; Exit can be followed only by Sub, Function, Property, Do, For, While, Select or Try (BC30240). Use Return inside a procedure, or restructure the block.

  • There is no equivalent of the C# 8 using declaration (using var x = ...;), which disposes at the end of the enclosing scope. A VB Using is always a block.

  • There is no Await Using (see IAsyncDisposable without Await Using), and no Using over a ref struct pattern-based Dispose, because Visual Basic cannot declare ref struct types.

Option Strict On

Public Class ShelfConnection
    Implements IDisposable

    Private ReadOnly _name As String

    Public Sub New(name As String)
        _name = name
        Console.WriteLine("open  " & _name)
    End Sub

    Public Sub Query(text As String)
        Console.WriteLine("query " & _name & ": " & text)
        If text = "boom" Then
            Throw New InvalidOperationException("query failed on " & _name)
        End If
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Console.WriteLine("close " & _name)
    End Sub
End Class

Module Program
    Sub Main()
        ' 1. Normal flow: two resources, disposed in reverse order of declaration.
        Using primary As New ShelfConnection("primary"), replica As New ShelfConnection("replica")
            primary.Query("SELECT 1")
            replica.Query("SELECT 2")
        End Using

        Console.WriteLine("---")

        ' 2. An exception leaves the block: Dispose still runs before the Catch.
        Try
            Using connection As New ShelfConnection("failing")
                connection.Query("boom")
                Console.WriteLine("not reached")
            End Using
        Catch ex As InvalidOperationException
            Console.WriteLine("caught: " & ex.Message)
        End Try

        Console.WriteLine("---")

        ' 3. A Using over an existing variable (the expression form) and over Nothing.
        Dim existing As New ShelfConnection("expression")
        Using existing
            existing.Query("SELECT 3")
        End Using

        Dim nothingHere As ShelfConnection = Nothing
        Using nothingHere
            Console.WriteLine("Nothing is allowed: no Dispose call, no exception")
        End Using
    End Sub
End Module
open  primary
open  replica
query primary: SELECT 1
query replica: SELECT 2
close replica
close primary
---
open  failing
query failing: boom
close failing
caught: query failed on failing
---
open  expression
query expression: SELECT 3
close expression
Nothing is allowed: no Dispose call, no exception

Source: Using Statement

The three scenarios cover the whole contract. In the first, the replica is closed before the primary. In the second, close failing is printed before caught, because the Finally that Using generates runs while the exception is propagating, before the Catch that handles it. In the third, the expression form disposes a variable that was created earlier, and Using over Nothing does nothing.

What the compiler rejects

Option Strict On

Public Class Tick
    Implements IDisposable
    Public Sub Dispose() Implements IDisposable.Dispose
    End Sub
End Class

Public Class NotDisposable
End Class

Module Program
    Sub Main()
        Using a As New NotDisposable()
        End Using
        Using t As New Tick()
            t = New Tick()
        End Using
        Using b As IAsyncDisposable = Nothing
        End Using
    End Sub
End Module

This program is meant to fail, so it was built with warnings not treated as errors and the project was not run. The compiler printed:

Program.vb(14,15): error BC36010: 'Using' operand of type 'NotDisposable' must implement 'System.IDisposable'.
Program.vb(17,13): error BC30064: 'ReadOnly' variable cannot be the target of an assignment.
Program.vb(19,15): error BC36010: 'Using' operand of type 'IAsyncDisposable' must implement 'System.IDisposable'.

The third error is the important one: IAsyncDisposable does not satisfy Using, because the statement only knows the synchronous interface. A type that implements both interfaces can be used in a Using, which then calls Dispose(), not DisposeAsync().

Where to put the Try

Using replaces the Finally, not the Catch. Put the Try ... Catch around the Using, so that the resource is already closed when the handler runs, as in scenario 2 above. Put the Using inside the Try for the usual cases, and inside a Catch only for resources that the handler itself needs (for example a log writer). Do not wrap a Using in a Try that catches Exception and swallows it: the resource is closed, but the error disappears (see Structured Exception Handling).

An exception from Dispose replaces the original one

If both the body and Dispose throw, the exception from Dispose wins and the original one is lost, because the exception thrown in the Finally replaces the exception in flight:

Option Strict On

Public Class Noisy
    Implements IDisposable

    Public Sub Dispose() Implements IDisposable.Dispose
        Throw New InvalidOperationException("Dispose failed")
    End Sub
End Class

Module Program
    Sub Main()
        Try
            Using n As New Noisy()
                Throw New ArgumentException("body failed")
            End Using
        Catch ex As Exception
            Console.WriteLine("caught " & ex.GetType().Name & ": " & ex.Message)
        End Try
    End Sub
End Module
caught InvalidOperationException: Dispose failed

The ArgumentException("body failed") thrown in the block never reaches the handler. This is why the guidelines say that Dispose should not throw: it must be callable from a cleanup path. Real libraries swallow expected I/O failures inside Dispose (for example when flushing a closed stream) for exactly this reason.

Deferred execution and a disposed resource

The most common Using bug in Visual Basic code that uses LINQ or iterators is returning a lazy sequence from inside the block. The block ends when the method returns, the resource is disposed, and the sequence is enumerated afterwards by the caller:

Option Strict On

Imports System.IO

Public Class Catalog
    Public Shared Function Titles(text As String) As IEnumerable(Of String)
        Using reader As New StringReader(text)
            Return ReadAll(reader)   ' deferred: the reader is disposed before anybody enumerates
        End Using
    End Function

    Private Shared Iterator Function ReadAll(reader As TextReader) As IEnumerable(Of String)
        Dim line = reader.ReadLine()
        While line IsNot Nothing
            Yield line
            line = reader.ReadLine()
        End While
    End Function

    Public Shared Function TitlesEager(text As String) As List(Of String)
        Using reader As New StringReader(text)
            Return ReadAll(reader).ToList()   ' materialised while the reader is open
        End Using
    End Function
End Class

Module Program
    Sub Main()
        Dim text = "Dune" & vbLf & "Emma"
        Console.WriteLine(String.Join(", ", Catalog.TitlesEager(text)))
        Try
            For Each title In Catalog.Titles(text)
                Console.WriteLine(title)
            Next
        Catch ex As ObjectDisposedException
            Console.WriteLine("lazy version failed: " & ex.GetType().Name)
        End Try
    End Sub
End Module
Dune, Emma
lazy version failed: ObjectDisposedException

Materialise (ToList, ToArray) before leaving the block, or return the resource’s owner so that the caller disposes it. The same applies to Iterator functions that wrap a Using (the Using inside an iterator is disposed when the iterator is disposed or completed, which a For Each does for you; see Iterators and Yield) and to Async methods that return before the awaited work that uses the resource has finished.

A constructor that throws

Using x As New T(...) calls the constructor before the protected region starts. If the constructor throws, there is no object to dispose, and Dispose is not called:

Option Strict On

Public Class Reservation
    Implements IDisposable

    Public Sub New(copies As Integer)
        Console.WriteLine("constructor starts")
        If copies < 1 Then
            Throw New ArgumentOutOfRangeException(NameOf(copies))
        End If
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Console.WriteLine("Dispose called")
    End Sub
End Class

Module Program
    Sub Main()
        Try
            Using r As New Reservation(0)
                Console.WriteLine("inside Using")
            End Using
        Catch ex As ArgumentOutOfRangeException
            Console.WriteLine("constructor failed; Dispose was not called: " & ex.ParamName)
        End Try
    End Sub
End Module
constructor starts
constructor failed; Dispose was not called: copies

Source: Using Statement

A constructor that allocates two resources therefore has to clean up the first itself when the second allocation fails (a Try ... Catch that disposes and rethrows inside Sub New), or must acquire the second one lazily. Listing two resources in one Using statement is safe when the second constructor fails, because the first is already protected; it does not help when a single constructor acquires two things.

Implementing IDisposable

Implement IDisposable when a class owns something that has to be released deterministically: a disposable field, an unmanaged handle, a subscription, a lock or a temporary file. Do not implement it for types that only borrow a disposable passed in from outside; the owner disposes it.

The simple case: a sealed class that owns managed disposables

Most classes only own other IDisposable objects. They need no finalizer and no Dispose(Boolean) method:

  • Mark the class NotInheritable, or the dispose method Overridable.

  • Dispose each owned field in Dispose, and make Dispose safe to call more than once.

  • Throw ObjectDisposedException from public members used after disposal.

The full dispose pattern

A class that is designed for inheritance, or that owns an unmanaged resource directly, uses the pattern below. The members are Public Sub Dispose() (the interface implementation), Protected Overridable Sub Dispose(disposing As Boolean) (the real work) and, only when a native handle is held directly, Protected Overrides Sub Finalize(). In Visual Basic the interface member is attached with Implements IDisposable.Dispose instead of C#'s implicit matching, and the overload Dispose(Boolean) shares the name with the parameterless one, which is allowed because overloads may differ by parameter list.

Option Strict On

' The dispose pattern for a class that owns an unmanaged handle (simulated here).
Public Class ScannerSession
    Implements IDisposable

    Private _disposed As Boolean
    Private _buffer As List(Of String) = New List(Of String)()   ' managed resource
    Private _handle As IntPtr = New IntPtr(42)                    ' stands in for an unmanaged resource

    Public Sub Scan(code As String)
        ObjectDisposedException.ThrowIf(_disposed, Me)
        _buffer.Add(code)
        Console.WriteLine("scanned " & code & " (" & _buffer.Count & " in buffer)")
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    Protected Overridable Sub Dispose(disposing As Boolean)
        If _disposed Then
            Return
        End If
        If disposing Then
            Console.WriteLine("Dispose(True): releasing managed state")
            _buffer.Clear()
        Else
            Console.WriteLine("Dispose(False): running from the finalizer")
        End If
        Console.WriteLine("releasing handle " & _handle.ToString())
        _handle = IntPtr.Zero
        _disposed = True
    End Sub

    Protected Overrides Sub Finalize()
        Try
            Dispose(False)
        Finally
            MyBase.Finalize()
        End Try
    End Sub
End Class

Public Class LoggingScannerSession
    Inherits ScannerSession

    Protected Overrides Sub Dispose(disposing As Boolean)
        Console.WriteLine("derived: disposing=" & disposing.ToString())
        MyBase.Dispose(disposing)
    End Sub
End Class

Module Program
    Sub Main()
        Dim session As New LoggingScannerSession()
        session.Scan("9780306406157")
        session.Dispose()
        session.Dispose()   ' idempotent: second call does nothing
        Try
            session.Scan("9780306406157")
        Catch ex As ObjectDisposedException
            Console.WriteLine("after Dispose: " & ex.GetType().Name & " for " & ex.ObjectName)
        End Try
    End Sub
End Module
scanned 9780306406157 (1 in buffer)
derived: disposing=True
Dispose(True): releasing managed state
releasing handle 42
derived: disposing=True
after Dispose: ObjectDisposedException for App.LoggingScannerSession

Points worth reading in the output:

  • The second session.Dispose() printed derived: disposing=True again but nothing else: the _disposed flag in the base class made the real work idempotent, while the overriding method in the derived class ran each time. A derived class must call MyBase.Dispose(disposing) and must not assume that its own override runs once.

  • Dispose() called GC.SuppressFinalize(Me), so no Dispose(False) line appears: the finalizer never ran. Without that call the object would stay in the finalization queue, survive one extra collection and run its cleanup a second time.

  • ObjectDisposedException.ThrowIf(_disposed, Me) is the idiomatic guard. It is a static method of the exception class, available since .NET 7, and it fills in ObjectName from the object’s type. Older code writes If _disposed Then Throw New ObjectDisposedException(Me.GetType().FullName).

Rule Reason

Dispose must be callable more than once

Using plus a manual call, or several owners, is common

Dispose(True) releases managed and unmanaged state; Dispose(False) only unmanaged state

a finalizer must not touch other managed objects, which may already be finalized

Do not throw from Dispose or Finalize (except for truly fatal conditions)

an exception in the finalizer thread ends the process; one in Dispose hides the original error

Call GC.SuppressFinalize(Me) in Dispose() only if the class has a finalizer

avoids a pointless trip through the finalization queue

Make the dispose-related members Protected Overridable, never Public

subclasses override the work; callers use the parameterless method

Do not implement a finalizer for a class that only owns managed disposables

it adds cost and risk for nothing

SafeHandle instead of a finalizer

Writing Finalize is rarely the right choice today. A class that wraps a native handle should hold a System.Runtime.InteropServices.SafeHandle (or one of its ready-made subclasses) in a field. SafeHandle has its own critical finalizer and is reference-counted across P/Invoke calls, so the owning class needs only the simple pattern. How handles are declared for Declare and DllImport calls is on Native Interop: Declare and P/Invoke.

Finalize

A finalizer is the method the collector calls on a separate thread after it has found the object unreachable. In Visual Basic it is a Protected Overrides Sub Finalize() that overrides Object.Finalize; there is no destructor syntax (~Name) as in C#.

  • The accessibility must be Protected, and it must override: declaring Public Sub Finalize() does not create a finalizer and the compiler reports a warning, BC40005 (the sub shadows the overridable base method).

  • Visual Basic does not insert the call to the base class’s finalizer for you (example below); the documented pattern calls MyBase.Finalize() in a Finally so that the chain runs even when your own cleanup throws.

  • The finalizers of a hierarchy run from the most derived class towards the base class, as the output of the first example shows.

  • Objects with a finalizer are allocated in a slower way, survive at least one extra collection and are promoted by it. Add one only when you hold an unmanaged resource that no SafeHandle covers.

  • The finalizer thread runs while your code runs. A finalizer must not rely on state that other finalizers may have already cleaned up, and it must not block.

Option Strict On

Public Class Tracked
    Private ReadOnly _name As String

    Public Sub New(name As String)
        _name = name
    End Sub

    Protected Overrides Sub Finalize()
        Try
            Console.WriteLine("finalizer of " & _name)
        Finally
            MyBase.Finalize()
        End Try
    End Sub
End Class

Public Class DerivedTracked
    Inherits Tracked

    Public Sub New()
        MyBase.New("derived")
    End Sub

    Protected Overrides Sub Finalize()
        Try
            Console.WriteLine("finalizer of DerivedTracked")
        Finally
            MyBase.Finalize()
        End Try
    End Sub
End Class

Module Program
    ' Allocation in its own method that is not inlined, so that no local keeps the object alive.
    <System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
    Private Sub Allocate()
        Dim unused As New DerivedTracked()
        Console.WriteLine("allocated, generation " & GC.GetGeneration(unused).ToString())
    End Sub

    Sub Main()
        Allocate()
        Console.WriteLine("before collect")
        GC.Collect()
        GC.WaitForPendingFinalizers()
        Console.WriteLine("after finalizers; gen0 collections so far >= 1: " & (GC.CollectionCount(0) >= 1).ToString())
    End Sub
End Module
allocated, generation 0
before collect
finalizer of DerivedTracked
finalizer of derived
after finalizers; gen0 collections so far >= 1: True

The method that allocates the object is marked NoInlining and returns before GC.Collect(), so that no local keeps it reachable. In a Debug build the JIT extends a local’s lifetime to the end of its method, which is why finalizer demos that allocate and collect in the same method often print nothing. GC.WaitForPendingFinalizers() blocks until the finalizer thread has drained the queue. Neither call belongs in production code.

The base-class chain is not automatic. If an override omits MyBase.Finalize(), the base class’s finalizer silently never runs:

Option Strict On

Public Class Tick
    Implements IDisposable
    Private ReadOnly _n As String
    Public Sub New(n As String)
        _n = n
    End Sub
    Public Sub Dispose() Implements IDisposable.Dispose
        Console.WriteLine("dispose " & _n)
    End Sub
End Class

Public Class BaseF
    Protected Overrides Sub Finalize()
        Console.WriteLine("BaseF.Finalize")
        MyBase.Finalize()
    End Sub
End Class

Public Class DerivedF
    Inherits BaseF
    Protected Overrides Sub Finalize()
        Console.WriteLine("DerivedF.Finalize without MyBase.Finalize call")
    End Sub
End Class

Module Program
    <System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
    Private Sub Allocate()
        Dim x As New DerivedF()
    End Sub

    Sub Main()
        Dim t As New Tick("first")
        Using t
            t = New Tick("second")
        End Using
        Console.WriteLine("t is now " & t.GetType().Name)
        Allocate()
        GC.Collect()
        GC.WaitForPendingFinalizers()
    End Sub
End Module
dispose first
t is now Tick
DerivedF.Finalize without MyBase.Finalize call

The output also shows two other facts. BaseF.Finalize is missing because the derived override never called it. And the expression form Using t disposed the first Tick (dispose first): the compiler had captured the value when the block started, so reassigning t inside the block did not redirect the disposal, and the second Tick is never disposed. The variable is not a safe place to swap resources.

NET Framework ran finalizers when the application domain was unloaded or the process exited. .NET (Core) does not run finalizers

on process exit: cleanup that must happen has to be done by Dispose, a Using or a handler for AppDomain.CurrentDomain.ProcessExit, not by relying on Finalize.

IAsyncDisposable without Await Using

Some types (streams with an asynchronous flush, database connections and contexts, asynchronous enumerators) implement IAsyncDisposable, whose method DisposeAsync returns a ValueTask. C# consumes it with await using. Visual Basic 17.13 has no such statement. Three language rules shape what you have to write instead.

  1. Using accepts only IDisposable (BC36010 above); it cannot call DisposeAsync.

  2. Await is not allowed inside a Finally or Catch block (BC36943), so the obvious replacement, Try ... Finally Await x.DisposeAsync() End Try, does not compile.

  3. An Async Function cannot return ValueTask (BC36945: only Sub, Task and Task(Of T)), so a VB class implementing IAsyncDisposable must write DisposeAsync as an ordinary function that wraps a Task.

The workaround is to remember the exception, dispose after the Try block and rethrow with ExceptionDispatchInfo, which keeps the original stack trace:

Option Strict On

Imports System.Runtime.ExceptionServices
Imports System.Threading.Tasks

Public Class AsyncShelf
    Implements IAsyncDisposable

    Public Async Function LoadAsync(fail As Boolean) As Task(Of Integer)
        Await Task.Delay(10)
        If fail Then
            Throw New InvalidOperationException("load failed")
        End If
        Return 3
    End Function

    ' Async Function ... As ValueTask is rejected by the compiler (BC36945): wrap a Task instead.
    Public Function DisposeAsync() As ValueTask Implements IAsyncDisposable.DisposeAsync
        Return New ValueTask(CloseAsync())
    End Function

    Private Shared Async Function CloseAsync() As Task
        Await Task.Delay(10)
        Console.WriteLine("DisposeAsync finished")
    End Function
End Class

Module Program
    Sub Main()
        Console.WriteLine(RunAsync(False).GetAwaiter().GetResult())
        Try
            RunAsync(True).GetAwaiter().GetResult()
        Catch ex As InvalidOperationException
            Console.WriteLine("rethrown after DisposeAsync: " & ex.Message)
        End Try
    End Sub

    ' There is no Await Using, and Await is not allowed in Finally (BC36943):
    ' remember the failure, dispose after the Try block, then rethrow.
    Private Async Function RunAsync(fail As Boolean) As Task(Of Integer)
        Dim shelf As New AsyncShelf()
        Dim result As Integer
        Dim failure As Exception = Nothing
        Try
            result = Await shelf.LoadAsync(fail)
        Catch ex As Exception
            failure = ex
        End Try
        Await shelf.DisposeAsync()
        If failure IsNot Nothing Then
            ExceptionDispatchInfo.Capture(failure).Throw()
        End If
        Return result
    End Function
End Module
DisposeAsync finished
3
DisposeAsync finished
rethrown after DisposeAsync: load failed

Types that implement both interfaces (most streams do) can be used with a normal Using; that calls Dispose(), which may block a thread while it flushes. For a method that is already asynchronous, prefer the explicit pattern above when the type’s DisposeAsync does real work. BC36943 and BC36945 had no page of their own on Learn when this page was checked; the rules above are the compiler’s own messages.

For Async/Await itself see Async and Await.

Nothing, Erase and the end of a variable’s life

Visual Basic programmers coming from VB6 often clear references out of habit. In .NET this rarely matters.

  • x = Nothing sets one variable to the default of its type. It does not free the object, does not call Dispose, and does not affect any other variable that refers to the same object.

  • A local variable that is no longer used is not a root in optimised code, so setting it to Nothing before the end of the method changes nothing. In a Debug build it changes how long the object is reachable, which is rarely what you want.

  • Setting a field or static to Nothing does matter: it removes a root that would otherwise live as long as its owner. That is how a long-lived cache or singleton releases data, and how a class breaks a cycle it created on purpose.

  • Erase array sets an array variable to Nothing. It releases nothing else, and Array.Clear is the call that resets elements without changing the variable.

WeakReference(Of T) is the supported way to hold an object without keeping it alive, for example in a cache the collector may trim:

Option Strict On

Public Class Weak
    ' A cache entry that the garbage collector may drop.
    Public Shared Sub Demo()
        Dim strong As New List(Of Integer) From {1, 2, 3}
        Dim weakRef As New WeakReference(Of List(Of Integer))(strong)
        Dim target As List(Of Integer) = Nothing
        Console.WriteLine("alive while referenced: " & weakRef.TryGetTarget(target).ToString())
        target = Nothing
        GC.KeepAlive(strong)   ' keeps strong reachable up to here, even in optimised code
    End Sub

    <System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
    Public Shared Function Create() As WeakReference(Of List(Of Integer))
        Return New WeakReference(Of List(Of Integer))(New List(Of Integer) From {1, 2, 3})
    End Function
End Class

Module Program
    Sub Main()
        Weak.Demo()

        Dim weakRef = Weak.Create()
        GC.Collect()
        GC.WaitForPendingFinalizers()
        Dim target As List(Of Integer) = Nothing
        Console.WriteLine("after collection: " & weakRef.TryGetTarget(target).ToString())

        ' Setting a local to Nothing does not free anything by itself; it only drops one reference.
        Dim big As Integer() = New Integer(99999) {}
        Dim alias1 = big
        big = Nothing
        Console.WriteLine("alias still sees " & alias1.Length.ToString() & " elements")

        ' Erase on an array variable sets the variable to Nothing.
        Dim numbers As Integer() = {1, 2, 3}
        Erase numbers
        Console.WriteLine("after Erase, numbers Is Nothing: " & (numbers Is Nothing).ToString())
    End Sub
End Module
alive while referenced: True
after collection: False
alias still sees 100000 elements
after Erase, numbers Is Nothing: True

Source: Weak References

The weak reference is True while strong is reachable and False after a collection once the only strong reference has gone. Setting big = Nothing left alias1 intact, which shows that Nothing removes a reference, not an object. Use TryGetTarget rather than a separate "is alive" check followed by a read, since the target can disappear between the two calls. Never use a weak reference to track the lifetime of an object whose cleanup matters; that is `Dispose’s job.

Source: Erase Statement

Event handlers keep objects alive

The garbage collector follows delegates. AddHandler publisher.Changed, AddressOf OnChanged stores a delegate inside the publisher that references the subscriber, so a subscriber stays reachable for as long as the publisher does. The same is true of WithEvents fields, which VB turns into AddHandler calls inside the property setter. A short-lived subscriber attached to a long-lived publisher (a static event, a singleton, a main window) therefore leaks until it calls RemoveHandler.

Option Strict On

Public Class Publisher
    Public Event Changed As EventHandler
End Class

Public Class Subscriber
    Public Sub New(source As Publisher)
        AddHandler source.Changed, AddressOf OnChanged
    End Sub

    Private Sub OnChanged(sender As Object, e As EventArgs)
    End Sub
End Class

Module Program
    <System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
    Private Function Attach(source As Publisher) As WeakReference
        Return New WeakReference(New Subscriber(source))
    End Function

    Sub Main()
        Dim publisher As New Publisher()
        Dim weak = Attach(publisher)
        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()
        Console.WriteLine("subscriber still alive after collection: " & weak.IsAlive.ToString())
        GC.KeepAlive(publisher)
    End Sub
End Module
subscriber still alive after collection: True

Nothing refers to the Subscriber object from the program except the event’s delegate list, and it survives the collection. The fix is symmetrical bookkeeping: RemoveHandler in Dispose (making the subscriber IDisposable), a weak-event pattern for UI frameworks, or a lifetime that is the same for publisher and subscriber. See Events and Handlers.

Structures, interfaces and the compiler

  • A Structure can implement IDisposable, but a structure is copied on assignment and when boxed to the interface, so a Dispose that changes the structure’s own fields may act on a copy; keep disposable types as classes. See Structures and Value Types.

  • Interface members that return a disposable resource must document who disposes it. A method named Open..., Create... or Get...Stream returns ownership to the caller, who must dispose; a property that exposes an owned field does not.

  • The IDisposable implementation of an interface-typed variable is not visible unless the interface inherits IDisposable; use TryCast(x, IDisposable) to dispose conditionally. See Interfaces.

  • Code analyzers report undisposed locals (CA2000), types that own disposable fields without implementing IDisposable (CA1001) and incorrect implementations (CA1063, CA2213, CA2215); enable them as described on XML Documentation, Analyzers and Code Style.

Typical resources and who disposes them

Resource What to do Page

FileStream, StreamReader, StreamWriter, StringReader

Using around every use; writers flush on Dispose

Files, Directories and Streams

DbConnection, DbCommand, DbDataReader, DbContext

Using per unit of work; dispose readers before reusing a connection

Data Access: ADO.NET and EF Core

HttpClient

create once and reuse, or obtain from IHttpClientFactory; do not Using a new one per request

Web, HTTP and Services

CancellationTokenSource, Timer, Process

Using; disposing a timer stops further callbacks

Threads, Parallelism and Synchronization

COM objects and Office automation

release explicitly with Marshal.ReleaseComObject or FinalReleaseComObject

COM Interop and Office Automation

Windows Forms controls and forms

a modal Form shown with ShowDialog must be disposed by the caller

Windows Forms Applications

Differences from C# and from older Visual Basic

Topic Visual Basic C#

Block form

Using x As New T() ... End Using

using (var x = new T()) { ... }

Declaration form (disposed at end of scope)

not available

using var x = new T();

Several resources

one statement, comma separated

nested blocks or a chain of using declarations

Async disposal

no Await Using; hand-written code, see above

await using

ref struct disposal

not applicable (no ref struct)

pattern-based Dispose on a ref struct

Finalizer

Protected Overrides Sub Finalize(), MyBase.Finalize() by hand

~ClassName(), base call inserted by the compiler

Field initializers vs base constructor

after MyBase.New returns

before the base constructor call

Exit from the block

no Exit Using; use Return

break/return as usual

Class_Terminate (VB6)

none; use Dispose

not applicable

Set x = Nothing (VB6)

only removes a reference

not applicable

Using and Nothing

a Nothing resource is skipped

using over null is skipped

Older material you may meet

Textbooks and tutorials written for the .NET Framework era describe a world that has mostly gone:

  • The Dispose pattern is still the same, but Finalize-based cleanup was often shown as the first line of defence. Today the preferred order is: use a Using; own a SafeHandle instead of a raw handle; write Finalize only as a last resort.

  • Examples that close files with FileClose/FileOpen and Set x = Nothing belong to the VB6 compatibility library (see The VB Family: VB6, VBA and VBScript). New code uses Using with StreamReader/StreamWriter.

  • Book listings that open a stream or File.Create without disposing it are leaking a handle and, for writers, may lose buffered data. The fix is always the same: put the object in a Using.

  • Singleton examples that rely on a finalizer to release resources at shutdown do not work on .NET (Core), which does not run finalizers at exit.

  • Marshal.ReleaseComObject examples for Office were written for a world where every COM reference had to be counted; the rules are on the COM page.

  • GC.Collect() "to free memory before an operation" appears in old tutorials; modern guidance is not to call it.

Pitfalls

  • Forgetting Using on a reader, writer, connection or process object. The file stays locked until the finalizer runs, which may be never in a short-lived process.

  • Returning a lazy IEnumerable(Of T) from inside a Using.

  • Throwing from Dispose, which masks the original exception.

  • Subscribing with AddHandler (or WithEvents) and never unsubscribing.

  • Calling an overridable member from Sub New.

  • Disposing an object you do not own, such as a stream passed in by a caller, or a shared HttpClient.

  • Expecting Await Using, or Await in Finally, to compile.

  • Relying on Finalize to run at process exit on .NET.

  • Using GC.Collect() or GC.WaitForPendingFinalizers() to "fix" a leak. A leak is a reachable object: find the root with a memory profiler (see Debugging and Diagnostics).