Async and Await

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 marked "built and run" is a complete console program that starts with Option Strict On, was built with warnings treated as errors (except where the text says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The root namespace of the scratch projects was App. No example needs the internet: the HTTP examples use either an in-process HttpMessageHandler that plays the part of the web service, or an HttpListener bound to the loopback address inside the same process. The Windows Forms example can only run on Windows and is labelled "built, not run": it was compiled against the net10.0-windows targeting pack and nothing more is claimed.

Asynchronous code lets a method start an operation that will take a while (a web request, a file read, a timer) and give the thread back to its caller until the operation finishes. Visual Basic has had Async and Await since Visual Basic 11 (Visual Studio 2012). The compiler rewrites an Async method into a state machine, so the code still reads top to bottom, with Try/Catch, loops and Using working as usual, while no thread is blocked during the waits. The running example is the shelf library catalogue, which looks up the title of a book by ISBN in a remote catalogue service. The platform side (the task-based asynchronous pattern, the thread pool and the state machine’s shape) is shared with every .NET language and is covered in more detail from the C# side in Async and Await (C#); this page is about what the Visual Basic compiler accepts, rejects and does differently. Threads, locks and parallel loops are in Threads, Parallelism and Synchronization.

The model in one picture

An Await is a pause with a promise to come back. The sequence below follows one click in a Windows Forms (or WPF) window that awaits an HttpClient call, which is the case that matters most because the code after the Await touches controls again. The important detail is the last three steps: the continuation does not run on whichever thread finished the download, it is posted to the SynchronizationContext that was current when the Await began, and that context runs it on the UI thread.

sequenceDiagram participant UI as UI thread (message loop) participant H as LookupClicked (Async Sub) participant C as HttpClient participant IO as Network and I/O completion participant SC as SynchronizationContext UI->>H: Click event H->>H: _lookup.Enabled = False H->>C: GetStringAsync(url) C->>IO: start the request (no thread is blocked) C-->>H: returns an incomplete Task(Of String) H-->>UI: Await saves the state and returns Note over UI: the UI keeps pumping messages IO-->>C: response arrives on a pool thread C->>SC: the task completes, Post(continuation) SC->>UI: the continuation is queued as a message UI->>H: resume after the Await (UI thread again) H->>H: _result.Text = body

Three facts in the diagram drive the rest of the page. The call that starts the operation returns before the operation has finished, handing back a Task that stands for the result. Await does not wait in the usual sense: if the task is not yet complete, the method returns to its caller and the rest of the method is scheduled as a continuation. And where the continuation runs is decided by the context captured at the Await, which is why a console program, a Windows Forms form and an ASP.NET Core request behave differently.

Async and Await in one example

An Async method is declared with the Async modifier and returns a Task or a Task(Of T) (or is a Sub). Inside it the Await operator takes a task and produces its result, without blocking. The caller of an Async Function receives the Task immediately, after the method has run synchronously up to its first Await that has to wait.

Built and run: an awaited HttpClient call against an in-process stub of the ISBN service
Option Strict On

Imports System.Net
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading

' An in-process stand-in for the ISBN web service: no network is needed.
Friend NotInheritable Class IsbnStubHandler
    Inherits HttpMessageHandler

    Protected Overrides Async Function SendAsync(request As HttpRequestMessage,
                                                 cancellationToken As CancellationToken) As Task(Of HttpResponseMessage)
        Await Task.Delay(50, cancellationToken)
        Dim isbn As String = request.RequestUri.Segments(request.RequestUri.Segments.Length - 1)
        If isbn = "0000000000000" Then
            Return New HttpResponseMessage(HttpStatusCode.NotFound)
        End If
        Dim json As String = "{""isbn"":""" & isbn & """,""title"":""Title of " & isbn & """}"
        Return New HttpResponseMessage(HttpStatusCode.OK) With {.Content = New StringContent(json)}
    End Function
End Class

Public Class BookInfo
    Public Property Isbn As String
    Public Property Title As String
End Class

Module Program
    Private ReadOnly Options As New JsonSerializerOptions(JsonSerializerDefaults.Web)

    Async Function LookupTitleAsync(client As HttpClient, isbn As String) As Task(Of String)
        Console.WriteLine("  LookupTitleAsync: sending the request")
        Dim json As String = Await client.GetStringAsync("https://catalog.example/isbn/" & isbn)
        Console.WriteLine("  LookupTitleAsync: the response arrived")
        Dim info As BookInfo = JsonSerializer.Deserialize(Of BookInfo)(json, Options)
        Return info.Title
    End Function

    Async Function RunAsync() As Task
        Using client As New HttpClient(New IsbnStubHandler())
            Console.WriteLine("RunAsync: calling LookupTitleAsync")
            Dim pending As Task(Of String) = LookupTitleAsync(client, "9780306406157")
            Console.WriteLine("RunAsync: the call returned a task, IsCompleted = " & pending.IsCompleted.ToString())
            Dim title As String = Await pending
            Console.WriteLine("RunAsync: title = " & title)
        End Using
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
        Console.WriteLine("Main: done")
    End Sub
End Module
RunAsync: calling LookupTitleAsync
  LookupTitleAsync: sending the request
RunAsync: the call returned a task, IsCompleted = False
  LookupTitleAsync: the response arrived
RunAsync: title = Title of 9780306406157
Main: done

Read the output as the control flow:

  1. RunAsync calls LookupTitleAsync, which runs synchronously until it reaches an Await on an incomplete task. It prints "sending the request", starts GetStringAsync and returns an incomplete Task(Of String) to RunAsync.

  2. RunAsync prints that the task is not complete (IsCompleted = False) and does its own Await, so it returns to Main, which is blocked in GetResult().

  3. 50 milliseconds later the stub answers, LookupTitleAsync resumes, parses the JSON and returns the title. That Return completes the task RunAsync is waiting for.

Four rules follow from this example and hold for every Async method.

  • Tasks are hot. Calling an Async Function starts the work. You do not start a task you got from such a method; you await it, or hold it to await later (to run several in parallel).

  • Return title returns a String, not a Task(Of String). The compiler wraps the value. In a function declared As Task there is no value and Return takes no operand (BC36952 when you try).

  • The method runs on the caller’s thread until the first Await that has to wait. An Async method is not a way to run code on another thread; it is a way to not hold a thread while waiting. For CPU-bound work use Task.Run (see CPU-bound work and Task.Run).

  • An exception thrown in the method is stored in the returned task. It appears at the Await, not at the call (see Exceptions in async code).

The JSON in the stub is parsed with System.Text.Json; the data types and serialization are covered in Serialization: JSON and XML.

Async return types

Declaration Result of Await Use

Async Function F() As Task

nothing; Await F() is a statement

an operation with no result; the caller can await it, wait for it and observe its failure

Async Function F() As Task(Of T)

a T

an operation with a result; Return value supplies the T

Async Sub F()

nothing is returned to the caller

event handlers only; failures cannot be observed (see Async Sub and event handlers)

Function F() As Task(Of T) (no Async)

a T

a method that only forwards or caches a task; it builds the task itself, with Task.FromResult, Task.Run or a TaskCompletionSource

Function F() As ValueTask(Of T) (no Async)

a T

consumed with Await; produced by hand, because an Async method cannot return it

anything else, including Async Function F() As Integer

not allowed

BC36945

Built and run: Task, Task(Of T), a hand-written task and a ValueTask(Of T) consumed with Await
Option Strict On

Module Program
    Async Function PlainTaskAsync() As Task
        Await Task.Delay(10)
        Console.WriteLine("PlainTaskAsync: finished")
    End Function

    Async Function CountAsync() As Task(Of Integer)
        Await Task.Delay(10)
        Return 42                              ' the operand is an Integer, not a Task(Of Integer)
    End Function

    ' Not Async: the method itself builds and returns the (already completed) task.
    Function CachedCount() As Task(Of Integer)
        Return Task.FromResult(7)
    End Function

    ' A ValueTask(Of T) can be produced by an ordinary function and consumed with Await.
    Function FastPath(useCache As Boolean) As ValueTask(Of Integer)
        If useCache Then
            Return New ValueTask(Of Integer)(5)
        End If
        Return New ValueTask(Of Integer)(CountAsync())
    End Function

    Async Function RunAsync() As Task
        Await PlainTaskAsync()
        Dim fromAsync As Integer = Await CountAsync()
        Dim fromCache As Integer = Await CachedCount()
        Dim fast As Integer = Await FastPath(True)
        Dim slow As Integer = Await FastPath(False)
        Console.WriteLine("values: " & fromAsync.ToString() & " " & fromCache.ToString() & " " &
                          fast.ToString() & " " & slow.ToString())

        Dim t As Task(Of Integer) = CountAsync()
        Console.WriteLine("type of the returned object: " & t.GetType().Name)
        Console.WriteLine("status before the await: " & t.Status.ToString())
        Dim awaited As Integer = Await t
        Console.WriteLine("status after the await: " & t.Status.ToString() & ", result " & awaited.ToString())
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
PlainTaskAsync: finished
values: 42 7 5 42
type of the returned object: AsyncStateMachineBox`1
status before the await: WaitingForActivation
status after the await: RanToCompletion, result 42

The compiler accepts only Task, Task(Of T) and Sub as the return types of an Async method. That excludes ValueTask and ValueTask(Of T) (an Async Function returning one is BC36945, shown below) and the "task-like" types that C# allows through AsyncMethodBuilder. Visual Basic code can still consume a ValueTask(Of T) returned by a library, awaiting it directly, and it can build one with the constructors that take a result or a Task, as FastPath does. A ValueTask must be awaited at most once; if you need to await it twice or combine it with WhenAll, call AsTask() first.

The task’s type name in the output, AsyncStateMachineBox`1, is an implementation detail of the runtime: the object returned by an Async Function is the box holding the compiler-generated state machine, and it derives from Task(Of T). Do not depend on it.

The Await operator accepts any expression whose type has a GetAwaiter method with the shape the compiler expects: Task, Task(Of T), ValueTask, ValueTask(Of T), Task.Yield(), the result of ConfigureAwait. As in C#, GetAwaiter may be an extension method, which lets you make an existing type awaitable:

Built and run: awaiting a TimeSpan through an extension GetAwaiter, and a non-generic TaskCompletionSource
Option Strict On

Imports System.Runtime.CompilerServices

Module TimeSpanAwaiter
    ' C# lets an extension method called GetAwaiter make any type awaitable. Does Visual Basic?
    <Extension>
    Function GetAwaiter(delay As TimeSpan) As TaskAwaiter
        Return Task.Delay(delay).GetAwaiter()
    End Function
End Module

Module Program
    Async Function RunAsync() As Task
        Dim source As New TaskCompletionSource()
        source.SetResult()
        Await source.Task
        Console.WriteLine("non-generic TaskCompletionSource works")

        Await TimeSpan.FromMilliseconds(10)
        Console.WriteLine("awaited a TimeSpan through an extension GetAwaiter")
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
non-generic TaskCompletionSource works
awaited a TimeSpan through an extension GetAwaiter

Source: Await Operator

The Await operator and where it may appear

Await is a unary operator that can be used only inside an Async method or an Async lambda. Outside one, the compiler does not even recognise it as an operator: Await is an ordinary identifier there, which is why the first message below is a generic syntax error rather than a message about Async.

Option Strict On

Module Program
    Sub Plain()
        Dim n As Integer = Await Task.FromResult(1)           ' not in an Async method
    End Sub

    Sub Main()
    End Sub
End Module
Program.vb(5,34): error BC30205: End of statement expected.

Source: Async

The same happens inside a lambda that is not itself Async. Declaring the lambda Async fixes it:

Option Strict On

Module Program
    Async Function InQuery() As Task
        Dim numbers As Integer() = {1, 2, 3}
        Dim doubled = numbers.Select(Function(n) Await Task.FromResult(n * 2))
        Await Task.Delay(1)
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(6,56): error BC32017: Comma, ')', or a valid expression continuation expected.

Await binds tighter than the arithmetic operators, so Await Compute() + 1 adds one to the awaited result, and because it applies to the whole operand that follows (a primary expression with its member accesses), a member of the result needs parentheses: (Await TitleAsync()).Length. Without them the compiler tries to read Length from the Task, as the last error in the next program shows.

Await inside Try, Catch, Finally and SyncLock

This is the largest difference from current C#. Await is allowed in the body of Try, in Using, in loops and in If statements, but it is not allowed in a Catch block, a Finally block or a SyncLock block:

Option Strict On

Module Program
    Async Function InCatchAndFinally() As Task
        Try
            Await Task.Delay(1)
            Throw New InvalidOperationException("boom")
        Catch ex As InvalidOperationException
            Await Task.Delay(1)
        Finally
            Await Task.Delay(1)
        End Try
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(9,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.
Program.vb(11,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.
Option Strict On

Module Program
    Private ReadOnly Gate As New Object()

    Async Function InLock() As Task
        SyncLock Gate
            Await Task.Delay(1)                               ' not inside SyncLock
        End SyncLock
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(8,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.

Source: Await Operator

The compiler reports the error at the Await; the message is the same for all three places (BC36943). C# has allowed await in catch and finally since C# 6 and it works in lock replacements such as SemaphoreSlim.WaitAsync. The Visual Basic workarounds are mechanical:

  • Catch: record the exception in a variable inside the Catch, leave the Try block, then Await the recovery code after End Try (the ForEachAsync helper later on this page captures the exception with ExceptionDispatchInfo for exactly this reason).

  • Finally: put the awaited cleanup after End Try, and arrange that every exit path reaches it. If the cleanup must run even on an exception, catch the exception, run the cleanup, then rethrow with ExceptionDispatchInfo.Capture(ex).Throw(), which keeps the original stack trace.

  • SyncLock: do not hold a monitor across an Await at all. A monitor belongs to a thread and the continuation may run on a different one. Use SemaphoreSlim(1, 1) and WaitAsync (see Limiting concurrency) or restructure so the locked region has no Await.

Console programs and the missing Async Main

A task that nobody waits for does not keep a process alive, so a console program needs something at the bottom of the call chain that blocks until the asynchronous work is done. C# 7.1 added async Task Main for this. Visual Basic has no such feature, in any language version, as of this writing:

Option Strict On

Module Program
    Async Function Main() As Task
        Await Task.Delay(1)
        Console.WriteLine("never runs")
    End Function
End Module
vbc : error BC30737: No accessible 'Main' method with an appropriate signature was found in 'App'.
Option Strict On

Module Program
    Async Sub Main()
        Await Task.Delay(1)
        Console.WriteLine("Async Sub Main continued after the Await")
    End Sub
End Module
Program.vb(4,15): error BC36934: The 'Main' method cannot be marked 'Async'.

Source: Async

The first program is the natural attempt (Async Function Main() As Task). The compiler does not treat it as an entry point at all, so it reports that no Main exists (BC30737). The second uses Async Sub Main, which the compiler refuses by name (BC36934). The pattern is a synchronous Main that starts the real work and blocks once, at the very edge of the program, with GetAwaiter().GetResult(), as every example on this page does:

Module Program
    Async Function RunAsync() As Task
        ' the whole program, written as ordinary Async code
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module

GetAwaiter().GetResult() is preferred over .Wait() and .Result in this one place because it rethrows the original exception instead of wrapping it in an AggregateException (shown in the next section). Blocking on a task is acceptable only here, at the top of a console program that has no synchronization context. Everywhere else, see [Deadlocks and sync-over-async].

When the application has a framework that owns Main (Windows Forms with the Application Framework, WPF, ASP.NET Core) you never write this bridge: the framework’s event loop or request pipeline is the one that runs Async event handlers and controllers.

Exceptions in async code

An exception thrown in an Async method does not unwind to the caller. It is caught by the generated state machine and stored in the returned task, which becomes Faulted. Await then rethrows it at the point where you await. Because Await rethrows the first inner exception of the task, you catch the same exception types you would catch from synchronous code. Wait() and Result behave differently: they throw an AggregateException that contains the original.

Built and run: Await versus Wait, WhenAll with several failures, validation inside an Async method, EnsureSuccessStatusCode
Option Strict On

Imports System.Net.Http

' Await rethrows the first exception unwrapped; Wait and Result wrap it in an AggregateException.
Module Program
    Async Function FailAsync(message As String, delayMs As Integer) As Task
        Await Task.Delay(delayMs)
        Throw New InvalidOperationException(message)
    End Function

    Async Function GuardedAsync(title As String) As Task(Of Integer)
        ' An exception thrown before the first Await still ends up inside the returned Task.
        ArgumentException.ThrowIfNullOrEmpty(title)
        Await Task.Delay(1)
        Return title.Length
    End Function

    Function NotAsync(title As String) As Task(Of Integer)
        ArgumentException.ThrowIfNullOrEmpty(title)   ' thrown synchronously, to the caller
        Return Task.FromResult(title.Length)
    End Function

    Async Function RunAsync() As Task
        Try
            Await FailAsync("one", 10)
        Catch ex As InvalidOperationException
            Console.WriteLine("Await rethrew: " & ex.GetType().Name & " '" & ex.Message & "'")
        End Try

        Try
            FailAsync("two", 10).Wait()
        Catch ex As AggregateException
            Console.WriteLine("Wait threw: " & ex.GetType().Name & " wrapping " & ex.InnerException.GetType().Name)
        End Try

        Dim all As Task = Task.WhenAll(FailAsync("first", 20), FailAsync("second", 10), FailAsync("third", 30))
        Try
            Await all
        Catch ex As InvalidOperationException
            Console.WriteLine("WhenAll + Await threw only: '" & ex.Message & "'")
        End Try
        Console.WriteLine("but the task holds " & all.Exception.InnerExceptions.Count.ToString() & " exceptions:")
        For Each inner As Exception In all.Exception.InnerExceptions
            Console.WriteLine("  " & inner.Message)
        Next

        Dim pending As Task(Of Integer) = GuardedAsync("")
        Console.WriteLine("GuardedAsync returned a task in state " & pending.Status.ToString())
        Try
            Await pending
        Catch ex As ArgumentException
            Console.WriteLine("  the exception surfaces at the Await: " & ex.GetType().Name)
        End Try

        Try
            Dim t As Task(Of Integer) = NotAsync("")
            Console.WriteLine("never printed " & t.Status.ToString())
        Catch ex As ArgumentException
            Console.WriteLine("NotAsync threw synchronously: " & ex.GetType().Name)
        End Try

        Dim client As New HttpClient(New StatusHandler())
        Try
            Dim response As HttpResponseMessage = Await client.GetAsync("https://catalog.example/missing")
            Console.WriteLine("GetAsync completed with " & CInt(response.StatusCode).ToString())
            response.EnsureSuccessStatusCode()
        Catch ex As HttpRequestException
            Console.WriteLine("EnsureSuccessStatusCode: " & ex.StatusCode.ToString())
        End Try
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module

Friend NotInheritable Class StatusHandler
    Inherits HttpMessageHandler

    Protected Overrides Function SendAsync(request As HttpRequestMessage,
                                           cancellationToken As Threading.CancellationToken) As Task(Of HttpResponseMessage)
        Return Task.FromResult(New HttpResponseMessage(Net.HttpStatusCode.NotFound))
    End Function
End Class
Await rethrew: InvalidOperationException 'one'
Wait threw: AggregateException wrapping InvalidOperationException
WhenAll + Await threw only: 'second'
but the task holds 3 exceptions:
  second
  first
  third
GuardedAsync returned a task in state Faulted
  the exception surfaces at the Await: ArgumentException
NotAsync threw synchronously: ArgumentException
GetAsync completed with 404
EnsureSuccessStatusCode: NotFound
  • Await throws the original type (InvalidOperationException); Wait() throws AggregateException.

  • With Task.WhenAll, all failures are kept in task.Exception.InnerExceptions, but Await throws only the first one. That first one is not necessarily the first argument: here it was second, which finished before first. To report every failure, keep the WhenAll task in a variable, await it inside Try, and read Exception.InnerExceptions in the Catch.

  • A guard clause such as ArgumentException.ThrowIfNullOrEmpty in an Async method does not throw at the call: the caller gets a faulted task and the exception appears at the Await. A method that validates arguments and then returns a task built without Async (NotAsync) throws synchronously. Either can be right; choose one convention for a code base and keep to it. Remember it when writing tests that expect the exception.

  • A completed HttpClient call with a 404 status is not an exception. The task completes with the response; throw with EnsureSuccessStatusCode() (which sets HttpRequestException.StatusCode), or use a helper such as GetFromJsonAsync, which does the same check.

  • A faulted task whose exception is never observed (never awaited, Wait`ed or read through `Exception) is reported through TaskScheduler.UnobservedTaskException when the task is garbage collected. Since .NET 4.5 that does not terminate the process, but it hides a bug: await every task you start, or store it somewhere that will.

Composing tasks

Awaiting tasks one after another runs them one after another. To run independent operations at the same time, start them all (calling the Async Function is what starts them), then await them together.

Built and run: sequential awaits against WhenAll, and WhenAny in completion order
Option Strict On

Imports System.Diagnostics
Imports System.Net.Http
Imports System.Threading

Friend NotInheritable Class DelayHandler
    Inherits HttpMessageHandler

    Protected Overrides Async Function SendAsync(request As HttpRequestMessage,
                                                 cancellationToken As CancellationToken) As Task(Of HttpResponseMessage)
        Dim isbn As String = request.RequestUri.Segments(request.RequestUri.Segments.Length - 1)
        Dim delay As Integer = If(isbn.EndsWith("1", StringComparison.Ordinal), 300, 100)
        Await Task.Delay(delay, cancellationToken)
        Return New HttpResponseMessage(Net.HttpStatusCode.OK) With {.Content = New StringContent("Title of " & isbn)}
    End Function
End Class

Module Program
    Async Function RunAsync() As Task
        Dim isbns As String() = {"9780000000011", "9780000000022", "9780000000033"}
        Using client As New HttpClient(New DelayHandler())
            Dim watch As Stopwatch = Stopwatch.StartNew()
            Dim sequential As New List(Of String)
            For Each isbn As String In isbns
                sequential.Add(Await client.GetStringAsync("https://catalog.example/isbn/" & isbn))
            Next
            Dim sequentialMs As Long = watch.ElapsedMilliseconds

            watch.Restart()
            Dim titles As String() = Await Task.WhenAll(isbns.Select(Function(isbn) client.GetStringAsync("https://catalog.example/isbn/" & isbn)))
            Dim concurrentMs As Long = watch.ElapsedMilliseconds

            Console.WriteLine("sequential:  " & String.Join(" | ", sequential) & "  (slower than 450 ms: " & (sequentialMs >= 450).ToString() & ")")
            Console.WriteLine("WhenAll:     " & String.Join(" | ", titles) & "  (faster than 450 ms: " & (concurrentMs < 450).ToString() & ")")

            ' WhenAny: the first task to finish, in completion order rather than list order.
            Dim tasks As List(Of Task(Of String)) = isbns.Select(Function(isbn) client.GetStringAsync("https://catalog.example/isbn/" & isbn)).ToList()
            Do While tasks.Count > 0
                Dim finished As Task(Of String) = Await Task.WhenAny(tasks)
                tasks.Remove(finished)
                Console.WriteLine("WhenAny: " & Await finished)
            Loop
        End Using
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
sequential:  Title of 9780000000011 | Title of 9780000000022 | Title of 9780000000033  (slower than 450 ms: True)
WhenAll:     Title of 9780000000011 | Title of 9780000000022 | Title of 9780000000033  (faster than 450 ms: True)
WhenAny: Title of 9780000000033
WhenAny: Title of 9780000000022
WhenAny: Title of 9780000000011

Points to notice:

  • The query isbns.Select(Function(isbn) client.GetStringAsync(...)) is lazy: nothing starts until WhenAll enumerates it. That is fine here, but a lazily started sequence enumerated twice would start the requests twice. Call .ToList() or .ToArray() first when you hold the tasks (as the WhenAny loop does).

  • Task.WhenAll returns the results in the order of the input, whichever task finished first.

  • Task.WhenAny returns the first task to finish; loop with it to process results as they complete. For many tasks the loop is quadratic; .NET 9 added Task.WhenEach, which yields tasks in completion order (shown under Consuming async streams).

  • There is no Await Task.WhenAll(a, b) returning a tuple. When the tasks have different result types, await the combined task and then read a.Result and b.Result, which are completed and therefore safe to read.

Limiting concurrency

Starting a thousand requests at once is rarely what you want. A SemaphoreSlim with WaitAsync is the asynchronous equivalent of a bounded lock. Note that the release goes in a Finally, which is fine because it contains no Await:

Built and run: at most three lookups at once
Option Strict On

Imports System.Threading

Module Program
    Private _active As Integer
    Private _peak As Integer

    Async Function LookupAsync(gate As SemaphoreSlim, isbn As String) As Task(Of String)
        Await gate.WaitAsync()
        Try
            Dim now As Integer = Interlocked.Increment(_active)
            Dim seen As Integer
            Do
                seen = Volatile.Read(_peak)
            Loop While now > seen AndAlso Interlocked.CompareExchange(_peak, now, seen) <> seen
            Await Task.Delay(30)
            Return "Title of " & isbn
        Finally
            Interlocked.Decrement(_active)
            gate.Release()
        End Try
    End Function

    Async Function RunAsync() As Task
        Dim isbns As IEnumerable(Of String) = Enumerable.Range(1, 12).Select(Function(i) "978000000" & i.ToString("D4"))
        Using gate As New SemaphoreSlim(3)
            Dim results As String() = Await Task.WhenAll(isbns.Select(Function(isbn) LookupAsync(gate, isbn)))
            Console.WriteLine(results.Length.ToString() & " lookups, never more than 3 at once: " & (_peak <= 3).ToString() & " (peak " & _peak.ToString() & ")")
        End Using
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
12 lookups, never more than 3 at once: True (peak 3)

Source: SemaphoreSlim

The peak of three is recorded with Interlocked and Volatile, described on the threads page. On .NET 6 and later Parallel.ForEachAsync does the same job with a built-in degree-of-parallelism option.

Cancellation and timeouts

Cancellation in .NET is cooperative. The caller creates a CancellationTokenSource, passes its Token down, and calls Cancel(). Every library method that accepts a token checks it and, when cancelled, completes its task in the Canceled state; awaiting that task throws OperationCanceledException (or the derived TaskCanceledException). Your own code takes part by calling token.ThrowIfCancellationRequested() in loops and by passing the token to the awaitable calls it makes.

Built and run: five ways to stop something that never answers
Option Strict On

Imports System.Net.Http
Imports System.Threading

Friend NotInheritable Class SlowHandler
    Inherits HttpMessageHandler

    Protected Overrides Async Function SendAsync(request As HttpRequestMessage,
                                                 cancellationToken As CancellationToken) As Task(Of HttpResponseMessage)
        Await Task.Delay(Timeout.Infinite, cancellationToken)   ' a server that never answers
        Return New HttpResponseMessage()
    End Function
End Class

Module Program
    Async Function RunAsync() As Task
        Using client As New HttpClient(New SlowHandler())
            ' 1. A CancellationTokenSource with a timeout.
            Using cts As New CancellationTokenSource(TimeSpan.FromMilliseconds(100))
                Try
                    Await client.GetStringAsync("https://catalog.example/slow", cts.Token)
                Catch ex As OperationCanceledException
                    Console.WriteLine("1. " & ex.GetType().Name & ", IsCancellationRequested = " & cts.IsCancellationRequested.ToString())
                End Try
            End Using

            ' 2. HttpClient.Timeout: also a TaskCanceledException, but with a TimeoutException inside.
            Using impatient As New HttpClient(New SlowHandler()) With {.Timeout = TimeSpan.FromMilliseconds(100)}
                Try
                    Await impatient.GetStringAsync("https://catalog.example/slow")
                Catch ex As TaskCanceledException
                    Console.WriteLine("2. " & ex.GetType().Name & " with inner " & ex.InnerException.GetType().Name)
                End Try
            End Using

            ' 3. WaitAsync gives up waiting for any task, but does not cancel the work.
            Dim work As Task(Of String) = client.GetStringAsync("https://catalog.example/slow")
            Try
                Await work.WaitAsync(TimeSpan.FromMilliseconds(100))
            Catch ex As TimeoutException
                Console.WriteLine("3. " & ex.GetType().Name & ", the work is still " & work.Status.ToString())
            End Try

            ' 4. Linked tokens: either the caller's token or our own limit stops the request.
            Using caller As New CancellationTokenSource()
                Using linked As CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(caller.Token)
                    linked.CancelAfter(TimeSpan.FromSeconds(30))
                    Dim request As Task(Of String) = client.GetStringAsync("https://catalog.example/slow", linked.Token)
                    caller.Cancel()
                    Try
                        Await request
                    Catch ex As OperationCanceledException
                        Console.WriteLine("4. caller cancelled: " & ex.CancellationToken.Equals(linked.Token).ToString() & ", status " & request.Status.ToString())
                    End Try
                End Using
            End Using
        End Using

        ' 5. Cooperative cancellation in our own code.
        Using cts As New CancellationTokenSource()
            Dim counter As Task(Of Integer) = CountUntilCancelledAsync(cts.Token)
            Await Task.Delay(60)
            cts.Cancel()
            Try
                Await counter
            Catch ex As OperationCanceledException
                Console.WriteLine("5. CountUntilCancelledAsync was cancelled, status " & counter.Status.ToString())
            End Try
        End Using
    End Function

    Async Function CountUntilCancelledAsync(token As CancellationToken) As Task(Of Integer)
        Dim n As Integer = 0
        Do
            token.ThrowIfCancellationRequested()
            Await Task.Delay(10, token)
            n += 1
        Loop
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
1. TaskCanceledException, IsCancellationRequested = True
2. TaskCanceledException with inner TimeoutException
3. TimeoutException, the work is still WaitingForActivation
4. caller cancelled: True, status Canceled
5. CountUntilCancelledAsync was cancelled, status Canceled

The five cases differ in what they stop:

Case Mechanism What it does to the work

1

New CancellationTokenSource(timeout) or cts.CancelAfter(timeout) and pass cts.Token

cancels the request itself, so the connection is released

2

HttpClient.Timeout

cancels the request; the exception is a TaskCanceledException whose InnerException is a TimeoutException, which is how you tell a timeout from a cancellation by the caller

3

Task.WaitAsync(TimeSpan)

only stops waiting; the task keeps running (here still WaitingForActivation). Use it when you cannot pass a token

4

CancellationTokenSource.CreateLinkedTokenSource

combines the caller’s token with a limit of your own; either one cancels

5

a loop with ThrowIfCancellationRequested and Task.Delay(ms, token)

cooperative cancellation of your own code

Dispose a CancellationTokenSource that has a timer (Using as above) so the timer is released. A task cancelled this way ends in the Canceled state, not Faulted, and a Catch ex As OperationCanceledException also catches TaskCanceledException, which derives from it.

Progress reporting

IProgress(Of T) decouples "report how far along you are" from "how the report is displayed". Progress(Of T) captures the SynchronizationContext that is current when it is created and posts each report to it, so a Progress(Of Integer) created on the UI thread updates controls safely.

Built and run: Progress(Of Integer) in a console program, where there is no context to post to
Option Strict On

Imports System.Threading

Module Program
    Async Function ImportAsync(progress As IProgress(Of Integer)) As Task(Of Integer)
        Dim imported As Integer = 0
        For i As Integer = 1 To 4
            Await Task.Delay(30)
            imported += 25
            If progress IsNot Nothing Then
                progress.Report(imported)
            End If
        Next
        Return imported
    End Function

    Async Function RunAsync() As Task
        ' Progress(Of T) posts the callback to the SynchronizationContext captured when it was created.
        ' A console program has none, so the callback runs on a thread-pool thread.
        Dim seen As New List(Of String)
        Dim progress As New Progress(Of Integer)(Sub(percent)
                                                    SyncLock seen
                                                        seen.Add(percent.ToString() & "%")
                                                    End SyncLock
                                                End Sub)
        Dim total As Integer = Await ImportAsync(progress)
        Await Task.Delay(100)                 ' let the posted callbacks run
        Console.WriteLine("result " & total.ToString() & ", progress: " & String.Join(" ", seen))
        Console.WriteLine("captured context in a console program: " & (SynchronizationContext.Current Is Nothing).ToString())
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
result 100, progress: 25% 50% 75% 100%
captured context in a console program: True

Source: Progress(Of T)

In a console program there is no captured context, so the callback runs on a thread-pool thread; two reports can then run at the same time or out of order, which is why the handler locks and why the example waits for the callbacks before printing. In a Windows Forms or WPF form the same code would run each callback on the UI thread, one at a time, in order.

Async lambdas and task sources

Lambdas can be Async, with Async Function (returns a task) or Async Sub (returns nothing). The rules for Await are the same as in named methods.

Built and run: Async lambdas, Task.Run with an Async lambda, Async Sub lambdas and a forwarded task
Option Strict On

Module Program
    Async Function RunAsync() As Task
        ' Dim f = Async Function ... infers an anonymous delegate type, not Func(Of Task(Of Integer)).
        Dim f = Async Function() As Task(Of Integer)
                    Await Task.Delay(5)
                    Return 10
                End Function
        Console.WriteLine("f is a Func(Of Task(Of Integer)): " & (TypeOf CObj(f) Is Func(Of Task(Of Integer))).ToString() & ", result " & (Await f()).ToString())

        Dim square As Func(Of Integer, Task(Of Integer)) = Async Function(x)
                                                                Await Task.Yield()
                                                                Return x * x
                                                            End Function
        Console.WriteLine("square(7) = " & (Await square(7)).ToString())

        ' Task.Run with an async lambda unwraps the inner task.
        Dim unwrapped As Integer = Await Task.Run(Async Function()
                                                      Await Task.Delay(5)
                                                      Return 99
                                                  End Function)
        Console.WriteLine("Task.Run(Async Function) = " & unwrapped.ToString())

        ' A single-line async lambda needs Await inside a Function body of one expression.
        Dim twice As Func(Of Integer, Task(Of Integer)) = Async Function(x) (Await square(x)) * 2
        Console.WriteLine("twice(3) = " & (Await twice(3)).ToString())

        ' An Async Sub lambda converts to Action: nobody can await it.
        Dim fireAndForget As Action = Async Sub()
                                          Await Task.Delay(20)
                                          Console.WriteLine("Async Sub lambda finished (nobody waited)")
                                      End Sub
        fireAndForget()
        Console.WriteLine("Action returned immediately")
        Await Task.Delay(100)

        ' Elided async: when a method only forwards a task, Async/Await adds a state machine for nothing.
        Console.WriteLine("forwarded: " & (Await ForwardAsync()).ToString())
    End Function

    Function ForwardAsync() As Task(Of Integer)
        Return Task.Run(Function() 123)
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
f is a Func(Of Task(Of Integer)): False, result 10
square(7) = 49
Task.Run(Async Function) = 99
twice(3) = 18
Action returned immediately
Async Sub lambda finished (nobody waited)
forwarded: 123
  • Dim f = Async Function() ... infers an anonymous delegate type, not Func(Of Task(Of Integer)). It converts to Func when you assign it to one, so give the variable a declared delegate type whenever you pass it on or store it in a field.

  • Task.Run(Async Function() ...) returns a task that represents the inner task as well, so awaiting it waits for the whole async operation. This is the overload resolution you want; the same call with an Async Sub lambda would not wait.

  • An Async Sub lambda assigned to Action returns at its first Await, exactly as the console output shows: "Action returned immediately" appears before the lambda’s own line. Failures in it cannot be observed (see below).

  • A method that only passes a task through (ForwardAsync) does not need Async/Await; omitting them avoids a state machine. The price is that an exception thrown before the task is returned is thrown to the caller, and that Using blocks around the call would dispose their resource before the task completes. Keep Async/Await whenever there is a Using or Try around the call.

Turning an event into a task

TaskCompletionSource lets you create a task whose completion you control: the bridge between callback or event style and Await. Visual Basic can use both TaskCompletionSource(Of T) and, since .NET 5, the non-generic TaskCompletionSource. Pass TaskCreationOptions.RunContinuationsAsynchronously so that code awaiting the task is not run inline on the thread that called SetResult, inside your event raiser or while it holds a lock.

Built and run: awaiting the next Loan.Returned event
Option Strict On

Imports System.Threading

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub
End Class

Module LoanExtensions
    ' Turn "the next time this event fires" into something that can be awaited.
    <Runtime.CompilerServices.Extension>
    Function NextReturnAsync(loan As Loan) As Task
        Dim source As New TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
        Dim handler As EventHandler = Nothing
        handler = Sub(sender As Object, e As EventArgs)
                      RemoveHandler loan.Returned, handler
                      source.TrySetResult()
                  End Sub
        AddHandler loan.Returned, handler
        Return source.Task
    End Function
End Module

Module Program
    Async Function RunAsync() As Task
        Dim loan As New Loan()
        Dim waiting As Task = loan.NextReturnAsync()
        Console.WriteLine("before Return(): waiting.IsCompleted = " & waiting.IsCompleted.ToString())
        Dim returner As Task = Task.Run(Async Function()
                                            Await Task.Delay(50)
                                            loan.Return()
                                        End Function)
        Await waiting
        Console.WriteLine("after the event: waiting.IsCompleted = " & waiting.IsCompleted.ToString())
        Await returner
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
before Return(): waiting.IsCompleted = False
after the event: waiting.IsCompleted = True

The helper unsubscribes itself from inside the handler, so it must refer to the delegate through a variable (a lambda cannot be removed by writing it again; see Events and Handlers).

Synchronization contexts, ConfigureAwait and deadlocks

When Await finds an incomplete task it captures SynchronizationContext.Current, or the current TaskScheduler when there is no context. After the task completes the continuation is posted there. The consequences depend on the application model:

Application model Context Continuation after Await

Console, services, unit tests (most), ASP.NET Core

none

a thread-pool thread

Windows Forms

WindowsFormsSynchronizationContext

the UI thread

WPF

DispatcherSynchronizationContext

the UI thread (the dispatcher)

ASP.NET (.NET Framework, not Core)

request context

any thread, but one at a time per request

The next program builds a small single-threaded context, the same shape as a UI thread, so the effect can be seen from a console. It runs three `Await`s and then deliberately blocks the "UI" thread.

Built and run: continuation placement with and without ConfigureAwait(False), and the classic deadlock
Option Strict On

Imports System.Collections.Concurrent
Imports System.Threading

' A tiny single-threaded SynchronizationContext, standing in for a UI thread.
Friend NotInheritable Class LoopContext
    Inherits SynchronizationContext

    Private ReadOnly _queue As New BlockingCollection(Of Action)()

    Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
        _queue.Add(Sub() d(state))
    End Sub

    Public Sub Complete()
        _queue.CompleteAdding()
    End Sub

    Public Sub RunLoop()
        SetSynchronizationContext(Me)
        For Each work As Action In _queue.GetConsumingEnumerable()
            work()
        Next
    End Sub
End Class

Module Program
    Private _context As LoopContext
    Private _loopThread As Integer

    Function OnLoopThread() As Boolean
        Return Environment.CurrentManagedThreadId = _loopThread
    End Function

    Async Function WithContextAsync() As Task
        Await Task.Delay(10)
    End Function

    Async Function LibraryStyleAsync() As Task
        Await Task.Delay(10).ConfigureAwait(False)
    End Function

    Async Function DemoAsync() As Task
        Console.WriteLine("start:                          on loop thread = " & OnLoopThread().ToString())

        Await Task.Delay(10)
        Console.WriteLine("after Await:                    on loop thread = " & OnLoopThread().ToString())

        Await Task.Delay(10).ConfigureAwait(False)
        Console.WriteLine("after ConfigureAwait(False):    on loop thread = " & OnLoopThread().ToString())
    End Function

    Async Function DeadlockAsync() As Task
        ' We are on the loop thread. Blocking it while the continuation needs it never completes.
        Dim finished As Boolean = WithContextAsync().Wait(300)
        Console.WriteLine("blocking on a method that resumes on the context: finished = " & finished.ToString())

        finished = LibraryStyleAsync().Wait(300)
        Console.WriteLine("blocking on a ConfigureAwait(False) method:       finished = " & finished.ToString())
        Await Task.CompletedTask
    End Function

    Sub Main()
        _context = New LoopContext()
        _loopThread = Environment.CurrentManagedThreadId

        _context.Post(Sub(state)
                          Dim chain As Task = RunAllAsync()
                          chain.ContinueWith(Sub(t) _context.Complete(), TaskScheduler.Default)
                      End Sub, Nothing)
        _context.RunLoop()
    End Sub

    Async Function RunAllAsync() As Task
        Await DemoAsync()
        Await DeadlockAsync()
    End Function
End Module
start:                          on loop thread = True
after Await:                    on loop thread = True
after ConfigureAwait(False):    on loop thread = False
blocking on a method that resumes on the context: finished = False
blocking on a ConfigureAwait(False) method:       finished = True
  • After a plain Await the code is back on the loop thread; after Await ... ConfigureAwait(False) it is on a pool thread.

  • WithContextAsync().Wait(300) blocked the loop thread while its continuation was queued on the loop thread. Neither could proceed; Wait timed out (finished = False). A real .Wait() or .Result without a time-out hangs forever. This is the sync-over-async deadlock and the most common async bug in UI code.

  • The same wait on a method that used ConfigureAwait(False) finished, because its continuation did not need the blocked thread.

Rules that follow:

  • Never block on a task from a thread that has a context (.Wait(), .Result, .GetAwaiter().GetResult()). Make the caller Async instead.

  • In library code, add .ConfigureAwait(False) to every Await. The library does not need the caller’s thread, and it protects callers who do block. In application code (an event handler that updates controls) do not use it.

  • ASP.NET Core and the console have no context, so ConfigureAwait(False) changes nothing there, but library code that targets several application models should still use it. .NET 8 added ConfigureAwait(ConfigureAwaitOptions) for finer control.

  • The ConfigureAwait call applies to one Await. After the first Await ... ConfigureAwait(False) the rest of the method runs without a context.

Windows Forms and WPF

In a form the rule is simple: write the handler Async Sub, Await the work, and use the controls after the Await. The continuation is posted back to the UI thread by the captured context, so no Invoke is needed. For CPU-bound work, Await Task.Run(...) moves the work to the pool and returns to the UI thread when it finishes.

Built, not run: an async Click handler in Windows Forms
Option Strict On

Imports System.Net.Http
Imports System.Windows.Forms

Public Class CatalogForm
    Inherits Form

    Private ReadOnly _client As New HttpClient()
    Private ReadOnly _lookup As New Button() With {.Text = "Look up", .Dock = DockStyle.Top}
    Private ReadOnly _result As New Label() With {.Dock = DockStyle.Fill}

    Public Sub New()
        Controls.Add(_result)
        Controls.Add(_lookup)
        AddHandler _lookup.Click, AddressOf LookupClicked
    End Sub

    ' Async Sub is acceptable here: it is an event handler.
    Private Async Sub LookupClicked(sender As Object, e As EventArgs)
        _lookup.Enabled = False
        Try
            _result.Text = "Looking up..."
            ' The continuation after the Await resumes on the UI thread, so the controls can be used.
            Dim body As String = Await _client.GetStringAsync("https://catalog.example/isbn/9780306406157")
            _result.Text = body

            ' CPU-bound work goes to the pool; the controls are touched again only after the Await.
            Dim words As Integer = Await Task.Run(Function() body.Split(" "c).Length)
            Text = words.ToString() & " words"
        Catch ex As HttpRequestException
            _result.Text = "Lookup failed: " & ex.Message
        Finally
            _lookup.Enabled = True
        End Try
    End Sub

    Protected Overrides Sub Dispose(disposing As Boolean)
        If disposing Then
            _client.Dispose()
        End If
        MyBase.Dispose(disposing)
    End Sub
End Class

Module Program
    <STAThread>
    Sub Main()
        Application.EnableVisualStyles()
        Application.Run(New CatalogForm())
    End Sub
End Module

This example compiles on the net10.0-windows target with UseWindowsForms and MyType set to WindowsFormsWithCustomSubMain (because it writes its own Sub Main); it was not run, because it needs a Windows desktop. The URL in it is a placeholder.

  • Async Sub is acceptable for this one case, an event handler, because the event delegate returns nothing. The Try/Catch is required: after the first Await an unhandled exception is posted to the context and, in Windows Forms, reaches Application.ThreadException.

  • Disable the control that started the operation (_lookup.Enabled = False) so the user cannot start a second copy while the first is running; async handlers are re-entrant because the UI keeps pumping messages.

  • In WPF the code is the same apart from the types: Async Sub Button_Click(sender As Object, e As RoutedEventArgs) and Dispatcher instead of Invoke when you must hop threads by hand. See WPF Applications and Windows Forms Applications.

HttpClient practice

HttpClient is the only HTTP client to use in new Visual Basic code; the older request classes of the Framework era remain only for compatibility and are marked obsolete or discouraged. The points that matter in practice:

  • Create one HttpClient and reuse it (a Shared ReadOnly field, or a typed client from IHttpClientFactory). One instance per request exhausts sockets under load and, because connections are pooled per handler, also ignores DNS changes. A long-lived client with a SocketsHttpHandler whose PooledConnectionLifetime is set handles both. The Using client As New HttpClient(...) in the examples on this page is for short programs only.

  • Dispose the HttpResponseMessage when you handle it yourself (Using response As HttpResponseMessage = Await client.GetAsync(...)). The GetStringAsync and GetFromJsonAsync helpers dispose it for you.

  • Prefer the helpers. GetStringAsync, GetByteArrayAsync, GetStreamAsync, GetFromJsonAsync(Of T) (namespace System.Net.Http.Json, part of the shared framework) and PostAsJsonAsync cover most calls. Use HttpRequestMessage when you need headers per request.

  • Check the status. GetAsync completes on any status; EnsureSuccessStatusCode() throws HttpRequestException with StatusCode for non-2xx responses, as does GetFromJsonAsync.

  • Every call should accept a CancellationToken. HttpClient.Timeout defaults to 100 seconds, which is rarely the right limit.

  • Read large bodies as streams (GetStreamAsync, or ResponseHeadersRead with GetAsync) instead of buffering a String.

The next program uses a real HTTP exchange over the loopback address in the same process, with HttpListener as the server, so that status codes, JSON and the content type are produced by the real stack.

Built and run: HttpClient against an HttpListener on loopback
Option Strict On

Imports System.Net
Imports System.Net.Http
Imports System.Net.Http.Json
Imports System.Net.Sockets
Imports System.Text

Public Class BookInfo
    Public Property Isbn As String
    Public Property Title As String
End Class

Module Program
    Function FreePort() As Integer
        Dim probe As New TcpListener(IPAddress.Loopback, 0)
        probe.Start()
        Dim port As Integer = CType(probe.LocalEndpoint, IPEndPoint).Port
        probe.Stop()
        Return port
    End Function

    Async Function ServeAsync(listener As HttpListener) As Task
        Do
            Dim context As HttpListenerContext
            Try
                context = Await listener.GetContextAsync()
            Catch ex As HttpListenerException
                Return
            Catch ex As ObjectDisposedException
                Return
            End Try
            Dim path As String = context.Request.Url.AbsolutePath
            If path.EndsWith("/missing", StringComparison.Ordinal) Then
                context.Response.StatusCode = 404
            Else
                Dim json As String = "{""isbn"":""9780306406157"",""title"":""A Real Title""}"
                Dim bytes As Byte() = Encoding.UTF8.GetBytes(json)
                context.Response.ContentType = "application/json"
                Await context.Response.OutputStream.WriteAsync(bytes)
            End If
            context.Response.Close()
        Loop
    End Function

    Async Function RunAsync() As Task
        Dim baseUrl As String = "http://127.0.0.1:" & FreePort().ToString() & "/"
        Dim listener As New HttpListener()
        listener.Prefixes.Add(baseUrl)
        listener.Start()
        Dim server As Task = ServeAsync(listener)

        Using client As New HttpClient() With {.BaseAddress = New Uri(baseUrl)}
            Dim info As BookInfo = Await client.GetFromJsonAsync(Of BookInfo)("isbn/9780306406157")
            Console.WriteLine("GetFromJsonAsync: " & info.Isbn & " = " & info.Title)

            Using response As HttpResponseMessage = Await client.GetAsync("isbn/missing")
                Console.WriteLine("status " & CInt(response.StatusCode).ToString() & " " & response.StatusCode.ToString() & ", IsSuccessStatusCode = " & response.IsSuccessStatusCode.ToString())
            End Using

            Try
                Await client.GetFromJsonAsync(Of BookInfo)("isbn/missing")
            Catch ex As HttpRequestException
                Console.WriteLine("GetFromJsonAsync on 404: HttpRequestException, StatusCode = " & ex.StatusCode.ToString())
            End Try

            Dim post As New StringContent("{""title"":""New""}", Encoding.UTF8, "application/json")
            Console.WriteLine("request content type: " & post.Headers.ContentType.ToString())
        End Using

        listener.Stop()
        listener.Close()
        Await server
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
GetFromJsonAsync: 9780306406157 = A Real Title
status 404 NotFound, IsSuccessStatusCode = False
GetFromJsonAsync on 404: HttpRequestException, StatusCode = NotFound
request content type: application/json; charset=utf-8

HttpListener is used here as a test server only; it works on Windows, Linux and macOS, but ASP.NET Core with Kestrel is the right choice for a real service (see Web, HTTP and Services). For production code, a typed client registered with IHttpClientFactory from Microsoft.Extensions.Http adds pooling, named configuration and resilience handlers; it is not shown here because it needs a NuGet package and a host.

Calling a real service from tests is slow and unreliable. The stub-handler technique used throughout this page is also the usual way to test code that takes an HttpClient: subclass HttpMessageHandler, override SendAsync, and pass the instance to the HttpClient constructor. (The override is itself written Protected Overrides Async Function SendAsync(...) As Task(Of HttpResponseMessage), which is legal.)

CPU-bound work and Task.Run

Async/Await is for waiting. It does not make computation faster, and it does not move computation to another thread.

Built and run: where the code runs after each kind of wait
Option Strict On

Imports System.Threading

Module Program
    Function Where() As String
        Return If(Thread.CurrentThread.IsThreadPoolThread, "pool thread", "main thread")
    End Function

    Async Function TitleAsync() As Task(Of String)
        Await Task.Delay(1)
        Return "Title of a book"
    End Function

    Async Function RunAsync() As Task
        Console.WriteLine("start:                     " & Where())

        ' I/O-bound work: no thread is blocked while waiting, nothing needs Task.Run.
        Await Task.Delay(20)
        Console.WriteLine("after Task.Delay:          " & Where())

        ' CPU-bound work: Task.Run moves it off the calling thread.
        Dim primes As Integer = Await Task.Run(Function()
                                                  Console.WriteLine("inside Task.Run:          " & Where())
                                                  Return Enumerable.Range(2, 20000).Count(Function(n) Enumerable.Range(2, CInt(Math.Sqrt(n)) - 1).All(Function(d) n Mod d <> 0))
                                              End Function)
        Console.WriteLine("primes below 20002:        " & primes.ToString())

        ' Await binds tighter than the arithmetic operators; use parentheses to call a member of the result.
        Dim length As Integer = (Await TitleAsync()).Length + 1
        Console.WriteLine("length + 1 =               " & length.ToString())

        ' Task.Yield forces the rest of the method to run as a new work item.
        Await Task.Yield()
        Console.WriteLine("after Task.Yield:          " & Where())
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
start:                     main thread
after Task.Delay:          pool thread
inside Task.Run:          pool thread
primes below 20002:        2262
length + 1 =               16
after Task.Yield:          pool thread
  • The program starts on the main thread. After Await Task.Delay it is on a pool thread (a console program has no context to return to).

  • Task.Run runs the lambda on a pool thread. Use it for CPU-bound work, on the caller’s side: expose the asynchronous operation (Function LoadAsync() As Task(Of T)) and let the caller decide whether to wrap CPU-bound work in Task.Run. A library method named ...Async that merely wraps a synchronous call in Task.Run misleads its callers about scalability.

  • Await Task.Yield() forces the rest of the method to be queued as a new work item, which is occasionally useful to keep a long loop from monopolising a context.

For parallel CPU-bound work (many items, many cores) prefer Parallel.For, Parallel.ForEach and PLINQ, covered on the threads page.

The state machine

The compiler rewrites each Async method into a type that implements IAsyncStateMachine. Locals and parameters become fields, each Await becomes a numbered state, and MoveNext is called to resume the method. The type is nested in the module or class and linked to the method through AsyncStateMachineAttribute; this is visible with reflection:

Built and run: the generated state machine, and Await taking the fast path
Option Strict On

Imports System.Reflection
Imports System.Runtime.CompilerServices

Module Program
    Async Function SlowAsync() As Task(Of Integer)
        Await Task.Delay(10)
        Return 1
    End Function

    Async Function FastAsync() As Task(Of Integer)
        Await Task.CompletedTask
        Return 2
    End Function

    Async Function RunAsync() As Task
        ' The compiler turns every Async method into a state machine type nested in the module.
        Dim method As MethodInfo = GetType(Program).GetMethod("SlowAsync", BindingFlags.NonPublic Or BindingFlags.Public Or BindingFlags.Static)
        Dim attribute As AsyncStateMachineAttribute = method.GetCustomAttribute(Of AsyncStateMachineAttribute)()
        Console.WriteLine("state machine: " & attribute.StateMachineType.Name & " (value type: " & attribute.StateMachineType.IsValueType.ToString() & ")")
        Console.WriteLine("implements IAsyncStateMachine: " & GetType(IAsyncStateMachine).IsAssignableFrom(attribute.StateMachineType).ToString())

        ' Await only suspends when the awaited task is not yet complete.
        Dim slow As Task(Of Integer) = SlowAsync()
        Dim fast As Task(Of Integer) = FastAsync()
        Console.WriteLine("SlowAsync completed synchronously: " & slow.IsCompleted.ToString())
        Console.WriteLine("FastAsync completed synchronously: " & fast.IsCompleted.ToString())
        Console.WriteLine("sum = " & ((Await slow) + (Await fast)).ToString())
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module
state machine: VB$StateMachine_0_SlowAsync (value type: True)
implements IAsyncStateMachine: True
SlowAsync completed synchronously: False
FastAsync completed synchronously: True
sum = 3
  • In a Release build the state machine is a structure (IsValueType is True), so an Async method that never actually suspends does not allocate for it; Debug builds use a class so that edit-and-continue works.

  • FastAsync awaited an already completed task, so Await did not suspend and the method finished before the call returned (completed synchronously: True). SlowAsync awaited an incomplete task and returned an incomplete task. Do not write code that depends on which case occurs.

  • Locals that cross an Await live in fields of the state machine, which is why ByRef parameters (BC36926) and ref-like types such cannot be used in Async methods.

Async Sub and event handlers

An Async Sub returns void to its caller, so nobody can await it, wait for it or catch its exceptions. Use it for one thing: event handlers, because the event’s delegate returns nothing. For everything else write Async Function ... As Task. The behaviour of Async Sub handlers (the publisher continues before the handler finishes, an unhandled exception after the first Await ends the process in a console program, the Try rule) is shown with built and run examples on Events and Handlers.

A related trap is passing an Async Sub where a Func(Of Task) was meant. Task.Run(Async Sub() ...) binds to the Action overload, so the returned task completes at the lambda’s first Await and its exceptions are lost. The compiler warns about it (BC42357, an error when warnings are errors); always use Async Function for anything you intend to await:

Option Strict On

Module Program
    Sub Main()
        ' Task.Run(Async Sub ...) binds to the Action overload: the returned task is complete when the lambda reaches its first Await.
        Dim t As Task = Task.Run(Async Sub()
                                     Await Task.Delay(100)
                                     Console.WriteLine("the Async Sub finished")
                                 End Sub)
        t.Wait()
        Console.WriteLine("Task.Run(Async Sub) completed early: " & t.IsCompletedSuccessfully.ToString())
        Threading.Thread.Sleep(300)
    End Sub
End Module
Program.vb(6,34): warning BC42357: Some overloads here take an Async Function rather than an Async Sub. Consider either using an Async Function, or casting this Async Sub explicitly to the desired type.

Consuming async streams

C# 8 added IAsyncEnumerable(Of T) and await foreach. Visual Basic has no Await For Each, no Await Using and no Async Iterator (the compiler rejects Async together with Iterator, BC36936):

Option Strict On

Module Program
    Async Iterator Function Numbers() As IAsyncEnumerable(Of Integer)
        Await Task.Delay(1)
        Yield 1
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(4,5): error BC36936: 'Async' and 'Iterator' modifiers cannot be used together.
Option Strict On

Module Program
    Async Function Resource() As Task
        Await Using r As New Threading.Timer(Nothing)
        End Using
        Await For Each x In Enumerable.Range(1, 3)
        Next
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(5,15): error BC30201: Expression expected.
Program.vb(6,9): error BC36007: 'End Using' must be preceded by a matching 'Using'.
Program.vb(7,15): error BC30201: Expression expected.
Program.vb(8,9): error BC30092: 'Next' must be preceded by a matching 'For'.

Source: Async

So Visual Basic code can consume an async sequence from a library (a Channel(Of T).Reader.ReadAllAsync(), Task.WhenEach, System.Linq.Async, EF Core’s AsAsyncEnumerable) only by driving the enumerator by hand, and can produce one only by returning a Channel reader or by writing the C# in a separate project (see Interoperating with C# and Other .NET Languages). The helper below hides the plumbing. Because Await is not allowed in Finally, it records a failure from the loop, disposes the enumerator after the Try, and then rethrows with the original stack trace.

Built and run: ForEachAsync over IAsyncEnumerable, Task.WhenEach and IAsyncDisposable
Option Strict On

Imports System.Runtime.ExceptionServices
Imports System.Threading
Imports System.Threading.Channels

Friend Module AsyncStreams
    ' VB has no "Await For Each": enumerate an IAsyncEnumerable(Of T) by hand.
    <Runtime.CompilerServices.Extension>
    Async Function ForEachAsync(Of T)(source As IAsyncEnumerable(Of T), body As Action(Of T)) As Task
        Dim enumerator As IAsyncEnumerator(Of T) = source.GetAsyncEnumerator()
        Dim failure As ExceptionDispatchInfo = Nothing
        Try
            Do While Await enumerator.MoveNextAsync()
                body(enumerator.Current)
            Loop
        Catch ex As Exception
            failure = ExceptionDispatchInfo.Capture(ex)      ' no Await is allowed in Catch or Finally
        End Try
        Await enumerator.DisposeAsync()                      ' so dispose after the Try block
        If failure IsNot Nothing Then
            failure.Throw()
        End If
    End Function
End Module

Module Program
    Async Function RunAsync() As Task
        Dim channel As Channel(Of String) = System.Threading.Channels.Channel.CreateUnbounded(Of String)()
        Dim producer As Task = Task.Run(Async Function()
                                            For Each isbn As String In {"9780000000011", "9780000000022", "9780000000033"}
                                                Await Task.Delay(10)
                                                Await channel.Writer.WriteAsync(isbn)
                                            Next
                                            channel.Writer.Complete()
                                        End Function)

        Await channel.Reader.ReadAllAsync().ForEachAsync(Sub(isbn) Console.WriteLine("received " & isbn))
        Await producer

        ' Task.WhenEach (.NET 9) yields tasks in completion order.
        Dim jobs As Task(Of String)() = {Delayed("slow", 90), Delayed("fast", 10), Delayed("medium", 50)}
        Dim order As New List(Of String)
        Await Task.WhenEach(jobs).ForEachAsync(Sub(job) order.Add(job.Result))
        Console.WriteLine("completion order: " & String.Join(", ", order))

        ' IAsyncDisposable: there is no "Await Using", so call DisposeAsync yourself.
        Dim resource As New AuditLog()
        Console.WriteLine("using the resource")
        Await resource.DisposeAsync()
    End Function

    Async Function Delayed(value As String, ms As Integer) As Task(Of String)
        Await Task.Delay(ms)
        Return value
    End Function

    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub
End Module

Friend NotInheritable Class AuditLog
    Implements IAsyncDisposable

    ' Async Function cannot return ValueTask, so wrap the Task that does the work.
    Public Function DisposeAsync() As ValueTask Implements IAsyncDisposable.DisposeAsync
        Return New ValueTask(FlushAsync())
    End Function

    Private Shared Async Function FlushAsync() As Task
        Await Task.Delay(1)
        Console.WriteLine("AuditLog flushed asynchronously")
    End Function
End Class
received 9780000000011
received 9780000000022
received 9780000000033
completion order: fast, medium, slow
using the resource
AuditLog flushed asynchronously
  • ForEachAsync is a generic extension method on IAsyncEnumerable(Of T); the body is a synchronous Action(Of T). An Func(Of T, Task) overload with an Await body(item) in the loop is the same shape.

  • The same technique awaits DisposeAsync for any IAsyncDisposable. A class that implements the interface cannot use an Async method for DisposeAsync (it returns ValueTask), so AuditLog wraps a Task in a ValueTask.

  • Task.WhenEach(jobs) (.NET 9) yields each task as it completes, so order is "fast, medium, slow" regardless of the array order.

Compiler messages seen while writing this page

All of these come from real builds with the .NET 10 SDK.

Code Message (shortened) Typical cause and fix

BC36945

The 'Async' modifier can only be used on Subs, or on Functions that return Task or Task(Of T).

Async Function ... As Integer, or As ValueTask(Of T). Return Task(Of Integer); for ValueTask, write a non-Async function.

BC36926

Async methods cannot have ByRef parameters.

Return the value (a Task(Of T) or a tuple) or wrap it in a small class.

BC36943

'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.

Move the awaited code after End Try/End SyncLock; use SemaphoreSlim.WaitAsync instead of SyncLock.

BC36930

'Await' requires that the type 'Integer' have a suitable GetAwaiter method.

Awaiting a value that is not a task. Remove the Await, or await the call that returns the task.

BC30491

Expression does not produce a value.

Awaiting a Sub call, or a plain Task assigned to a variable: Dim n As Integer = Await Task.Delay(1).

BC30311

Value of type 'Task(Of String)' cannot be converted to 'String'.

A missing Await. With Option Strict On this is an error; with Off it is a run-time cast failure.

BC30456

'Length' is not a member of 'Task(Of String)'.

Await TitleAsync().Length; write (Await TitleAsync()).Length.

BC36952

'Return' statements in this Async method cannot return a value since the return type of the function is 'Task'.

Change the return type to Task(Of T) or drop the value.

BC36934

The 'Main' method cannot be marked 'Async'.

Use Sub Main and RunAsync().GetAwaiter().GetResult().

BC30737

No accessible 'Main' method with an appropriate signature was found.

Async Function Main() As Task is not an entry point.

BC36936

'Async' and 'Iterator' modifiers cannot be used together.

Visual Basic cannot declare async iterators; return a Channel reader or use C#.

BC30205 / BC32017

End of statement expected. / Comma, ')', or a valid expression continuation expected.

Await in a method or lambda that is not Async: add the modifier.

BC42358 (warning)

Because this call is not awaited, execution of the current method continues before the call is completed.

A task-returning call used as a statement. Await it, assign it (Dim t = ...), or discard it deliberately.

BC42356 (warning)

This async method lacks 'Await' operators and so will run synchronously.

An Async method with no Await. Remove Async and return Task.CompletedTask or Task.FromResult.

BC42357 (warning)

Some overloads here take an Async Function rather than an Async Sub.

Task.Run(Async Sub() ...): use Async Function, or cast the lambda explicitly.

BC42105 (warning)

Function 'NeedsValue' doesn’t return a value on all code paths.

An Async Function ... As Task(Of T) that ends without Return value; the result is the default value of T.

The errors are shown together in one program that fails to compile (declaration errors hide body errors, so the programs on this page show them in separate groups):

Option Strict On

Module Program
    Async Function NotATask() As Integer          ' must return Task or Task(Of T)
        Return 1
    End Function

    Async Function WithByRef(ByRef counter As Integer) As Task   ' no ByRef parameters
        Await Task.Delay(1)
    End Function

    Async Function ValueTaskResult() As ValueTask(Of Integer)    ' ValueTask cannot be produced by Async
        Await Task.Delay(1)
        Return 1
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(4,34): error BC36945: The 'Async' modifier can only be used on Subs, or on Functions that return Task or Task(Of T).
Program.vb(8,30): error BC36926: Async methods cannot have ByRef parameters.
Program.vb(12,41): error BC36945: The 'Async' modifier can only be used on Subs, or on Functions that return Task or Task(Of T).
Option Strict On

Module Program
    Async Function TitleAsync() As Task(Of String)
        Await Task.Delay(1)
        Return "Title"
    End Function

    Sub Log(message As String)
    End Sub

    Async Function Broken() As Task
        Dim a As Integer = Await 5                              ' not awaitable
        Await Log("x")                                          ' a Sub has no value to await
        Dim n As Integer = Await Task.Delay(1)                  ' plain Task has no result
        Dim forgot As String = TitleAsync()                     ' forgot the Await
        Dim length As Integer = Await TitleAsync().Length       ' Await binds to the whole operand
    End Function

    Async Function NoValue() As Task
        Return 1                                                ' Task has no result value
    End Function

    Async Function NeedsValue() As Task(Of Integer)
        Await Task.Delay(1)
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(13,28): error BC36930: 'Await' requires that the type 'Integer' have a suitable GetAwaiter method.
Program.vb(14,15): error BC30491: Expression does not produce a value.
Program.vb(15,28): error BC30491: Expression does not produce a value.
Program.vb(16,32): error BC30311: Value of type 'Task(Of String)' cannot be converted to 'String'.
Program.vb(17,39): error BC30456: 'Length' is not a member of 'Task(Of String)'.
Program.vb(21,9): error BC36952: 'Return' statements in this Async method cannot return a value since the return type of the function is 'Task'. Consider changing the function's return type to 'Task(Of T)'.
Program.vb(26,5): warning BC42105: Function 'NeedsValue' doesn't return a value on all code paths. A null reference exception could occur at run time when the result is used.

The two warnings are only warnings, which is dangerous when a project does not treat warnings as errors: a forgotten Await compiles and then runs the call concurrently, losing its result and its exception. This program is built with warnings not treated as errors to show them:

Option Strict On

Module Program
    Async Function FetchAsync() As Task(Of Integer)
        Await Task.Delay(1)
        Return 1
    End Function

    Async Function NoAwaitAsync() As Task
        Console.WriteLine("no Await in here")
    End Function

    Async Function Caller() As Task
        FetchAsync()                      ' result and task ignored
        Await Task.Delay(1)
    End Function

    Sub Main()
        Dim t As Task = Caller()
        FetchAsync()
    End Sub
End Module
Program.vb(9,20): warning BC42356: This async method lacks 'Await' operators and so will run synchronously. Consider using the 'Await' operator to await non-blocking API calls, or 'Await Task.Run(...)' to do CPU-bound work on a background thread.
Program.vb(14,9): warning BC42358: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the Await operator to the result of the call.
Program.vb(20,9): warning BC42358: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the Await operator to the result of the call.

Source: Warning BC42358

Treat BC42358 and BC42356 as errors with <WarningsAsErrors> (see Projects, MSBuild and the dotnet CLI).

Visual Basic versus C#

Topic Visual Basic C#

Keywords

Async, Await (VB 11, Visual Studio 2012)

async, await (C# 5)

Entry point

Sub Main only; Async Sub Main is BC36934, Async Function Main is not an entry point

static async Task Main (C# 7.1)

Await in Catch/Finally

BC36943

allowed (C# 6)

Await inside SyncLock / lock

BC36943

compile error CS1996

Async return types

Task, Task(Of T), Sub

also ValueTask, ValueTask<T>, IAsyncEnumerable<T>, custom task-like types

Async iterators, await foreach, await using

none (Async with Iterator is BC36936; consume by hand)

C# 8

ByRef / ref parameters

BC36926

CS1988

Async lambda type

anonymous delegate with Dim f = Async Function() ...

Func<Task<int>> inferred

Awaiting a Sub call

BC30491

CS4008

Not awaited call

warning BC42358

warning CS4014

async void

Async Sub

async void

Extension GetAwaiter

works

works

Task.Run(Async Function() ...)

unwraps the inner task

unwraps the inner task

Parentheses around Await

(Await x).Member

(await x).Member

Older material you may meet

  • Visual Basic 2010 and earlier used the Begin/End asynchronous pattern, BackgroundWorker and raw threads. They still compile, but Async/Await replaces them (see the threads page for BackgroundWorker). Delegate BeginInvoke and EndInvoke throw PlatformNotSupportedException on .NET 5 and later.

  • Books and blog posts that show Async Sub Main or Async Function Main for console programs do not compile with any Visual Basic version (BC36934, BC30737): use the GetAwaiter().GetResult() bridge.

  • Samples that fetch pages with the older HTTP classes belong to the .NET Framework era; the HttpClient code here is the current form. Samples that create a new HttpClient per request in a Using block, as the examples here do for brevity, are a known source of socket exhaustion in services.

  • Code from the Framework era that calls .Result or .Wait() inside a UI or ASP.NET event handler works only by luck; it is the deadlock shown above. Make the handler Async.

  • Some tutorials for real-time apps (SignalR chat samples, for instance) show console clients with Async Sub Main; the equivalent is the Sub Main bridge with RunAsync().GetAwaiter().GetResult().

Pitfalls checklist

Pitfall What to do

Async Sub anywhere but an event handler

Use Async Function ... As Task.

Blocking on a task (.Result, .Wait(), GetAwaiter().GetResult()) on a thread with a context

Make the caller Async; in libraries use ConfigureAwait(False).

Forgetting Await (BC42358)

Treat the warning as an error. The call runs, the result and failure are lost.

Await in Catch, Finally or SyncLock (BC36943)

Capture, leave the block, then await.

Holding a SyncLock across an Await

Use SemaphoreSlim(1, 1) and WaitAsync.

Throwing exceptions in a method named ...Async before returning the task

Decide whether validation throws synchronously or via the task, and be consistent.

Task.WhenAll hides all but the first exception at the Await

Keep the WhenAll task and read Exception.InnerExceptions.

A new HttpClient for every call

Reuse one, or use IHttpClientFactory.

No time-out or token on network calls

Pass a CancellationToken; set HttpClient.Timeout deliberately.

Task.Run around an I/O call

Remove it; Await the I/O method directly.

Capturing the loop variable of For in an async lambda

Copy it to a local first (BC42324).

Unobserved exceptions in tasks that were never awaited

Await, store or ContinueWith every task you start.

Dim f = Async Function() ... passed where a Func(Of Task) is needed

Declare Dim f As Func(Of Task) = Async Function() ....

Calling Task.Result before completion in "fire and forget" code

Await, or check IsCompleted.