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.
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.
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:
-
RunAsynccallsLookupTitleAsync, which runs synchronously until it reaches anAwaiton an incomplete task. It prints "sending the request", startsGetStringAsyncand returns an incompleteTask(Of String)toRunAsync. -
RunAsyncprints that the task is not complete (IsCompleted = False) and does its ownAwait, so it returns toMain, which is blocked inGetResult(). -
50 milliseconds later the stub answers,
LookupTitleAsyncresumes, parses the JSON and returns the title. ThatReturncompletes the taskRunAsyncis waiting for.
Four rules follow from this example and hold for every Async method.
-
Tasks are hot. Calling an
Async Functionstarts 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 titlereturns aString, not aTask(Of String). The compiler wraps the value. In a function declaredAs Taskthere is no value andReturntakes no operand (BC36952 when you try). -
The method runs on the caller’s thread until the first
Awaitthat has to wait. AnAsyncmethod 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 useTask.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 |
|---|---|---|
|
nothing; |
an operation with no result; the caller can await it, wait for it and observe its failure |
|
a |
an operation with a result; |
|
nothing is returned to the caller |
event handlers only; failures cannot be observed (see Async Sub and event handlers) |
|
a |
a method that only forwards or caches a task; it builds the task itself, with |
|
a |
consumed with |
anything else, including |
not allowed |
BC36945 |
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:
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 theCatch, leave theTryblock, thenAwaitthe recovery code afterEnd Try(theForEachAsynchelper later on this page captures the exception withExceptionDispatchInfofor exactly this reason). -
Finally: put the awaited cleanup afterEnd 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 withExceptionDispatchInfo.Capture(ex).Throw(), which keeps the original stack trace. -
SyncLock: do not hold a monitor across anAwaitat all. A monitor belongs to a thread and the continuation may run on a different one. UseSemaphoreSlim(1, 1)andWaitAsync(see Limiting concurrency) or restructure so the locked region has noAwait.
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.
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
-
Awaitthrows the original type (InvalidOperationException);Wait()throwsAggregateException. -
With
Task.WhenAll, all failures are kept intask.Exception.InnerExceptions, butAwaitthrows only the first one. That first one is not necessarily the first argument: here it wassecond, which finished beforefirst. To report every failure, keep theWhenAlltask in a variable, await it insideTry, and readException.InnerExceptionsin theCatch. -
A guard clause such as
ArgumentException.ThrowIfNullOrEmptyin anAsyncmethod does not throw at the call: the caller gets a faulted task and the exception appears at theAwait. A method that validates arguments and then returns a task built withoutAsync(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
HttpClientcall with a404status is not an exception. The task completes with the response; throw withEnsureSuccessStatusCode()(which setsHttpRequestException.StatusCode), or use a helper such asGetFromJsonAsync, which does the same check. -
A faulted task whose exception is never observed (never awaited,
Wait`ed or read through `Exception) is reported throughTaskScheduler.UnobservedTaskExceptionwhen 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.
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 untilWhenAllenumerates 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 theWhenAnyloop does). -
Task.WhenAllreturns the results in the order of the input, whichever task finished first. -
Task.WhenAnyreturns the first task to finish; loop with it to process results as they complete. For many tasks the loop is quadratic; .NET 9 addedTask.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 reada.Resultandb.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:
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.
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 |
|
cancels the request itself, so the connection is released |
2 |
|
cancels the request; the exception is a |
3 |
|
only stops waiting; the task keeps running (here still |
4 |
|
combines the caller’s token with a limit of your own; either one cancels |
5 |
a loop with |
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.
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.
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
Source: Lambda Expressions
-
Dim f = Async Function() ...infers an anonymous delegate type, notFunc(Of Task(Of Integer)). It converts toFuncwhen 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 anAsync Sublambda would not wait. -
An
Async Sublambda assigned toActionreturns at its firstAwait, 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 needAsync/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 thatUsingblocks around the call would dispose their resource before the task completes. KeepAsync/Awaitwhenever there is aUsingorTryaround 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.
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
Source: TaskCompletionSource(Of TResult)
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 |
|
the UI thread |
WPF |
|
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.
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
Source: SynchronizationContext
-
After a plain
Awaitthe code is back on the loop thread; afterAwait ... 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;Waittimed out (finished = False). A real.Wait()or.Resultwithout 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 callerAsyncinstead. -
In library code, add
.ConfigureAwait(False)to everyAwait. 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 addedConfigureAwait(ConfigureAwaitOptions)for finer control. -
The
ConfigureAwaitcall applies to oneAwait. After the firstAwait ... 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.
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 Subis acceptable for this one case, an event handler, because the event delegate returns nothing. TheTry/Catchis required: after the firstAwaitan unhandled exception is posted to the context and, in Windows Forms, reachesApplication.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)andDispatcherinstead ofInvokewhen 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
HttpClientand reuse it (aShared ReadOnlyfield, or a typed client fromIHttpClientFactory). One instance per request exhausts sockets under load and, because connections are pooled per handler, also ignores DNS changes. A long-lived client with aSocketsHttpHandlerwhosePooledConnectionLifetimeis set handles both. TheUsing client As New HttpClient(...)in the examples on this page is for short programs only. -
Dispose the
HttpResponseMessagewhen you handle it yourself (Using response As HttpResponseMessage = Await client.GetAsync(...)). TheGetStringAsyncandGetFromJsonAsynchelpers dispose it for you. -
Prefer the helpers.
GetStringAsync,GetByteArrayAsync,GetStreamAsync,GetFromJsonAsync(Of T)(namespaceSystem.Net.Http.Json, part of the shared framework) andPostAsJsonAsynccover most calls. UseHttpRequestMessagewhen you need headers per request. -
Check the status.
GetAsynccompletes on any status;EnsureSuccessStatusCode()throwsHttpRequestExceptionwithStatusCodefor non-2xx responses, as doesGetFromJsonAsync. -
Every call should accept a
CancellationToken.HttpClient.Timeoutdefaults to 100 seconds, which is rarely the right limit. -
Read large bodies as streams (
GetStreamAsync, orResponseHeadersReadwithGetAsync) instead of buffering aString.
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.
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
Source: Guidelines for using HttpClient
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.
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.Delayit is on a pool thread (a console program has no context to return to). -
Task.Runruns 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 inTask.Run. A library method named...Asyncthat merely wraps a synchronous call inTask.Runmisleads 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:
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
Source: AsyncStateMachineAttribute
-
In a Release build the state machine is a structure (
IsValueTypeisTrue), so anAsyncmethod that never actually suspends does not allocate for it; Debug builds use a class so that edit-and-continue works. -
FastAsyncawaited an already completed task, soAwaitdid not suspend and the method finished before the call returned (completed synchronously: True).SlowAsyncawaited an incomplete task and returned an incomplete task. Do not write code that depends on which case occurs. -
Locals that cross an
Awaitlive in fields of the state machine, which is whyByRefparameters (BC36926) and ref-like types such cannot be used inAsyncmethods.
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.
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
Source: IAsyncEnumerable(Of T)
-
ForEachAsyncis a generic extension method onIAsyncEnumerable(Of T); the body is a synchronousAction(Of T). AnFunc(Of T, Task)overload with anAwait body(item)in the loop is the same shape. -
The same technique awaits
DisposeAsyncfor anyIAsyncDisposable. A class that implements the interface cannot use anAsyncmethod forDisposeAsync(it returnsValueTask), soAuditLogwraps aTaskin aValueTask. -
Task.WhenEach(jobs)(.NET 9) yields each task as it completes, soorderis "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). |
|
BC36926 |
Async methods cannot have ByRef parameters. |
Return the value (a |
BC36943 |
'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement. |
Move the awaited code after |
BC36930 |
'Await' requires that the type 'Integer' have a suitable GetAwaiter method. |
Awaiting a value that is not a task. Remove the |
BC30491 |
Expression does not produce a value. |
Awaiting a |
BC30311 |
Value of type 'Task(Of String)' cannot be converted to 'String'. |
A missing |
BC30456 |
'Length' is not a member of 'Task(Of String)'. |
|
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 |
BC36934 |
The 'Main' method cannot be marked 'Async'. |
Use |
BC30737 |
No accessible 'Main' method with an appropriate signature was found. |
|
BC36936 |
'Async' and 'Iterator' modifiers cannot be used together. |
Visual Basic cannot declare async iterators; return a |
BC30205 / BC32017 |
End of statement expected. / Comma, ')', or a valid expression continuation expected. |
|
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 ( |
BC42356 (warning) |
This async method lacks 'Await' operators and so will run synchronously. |
An |
BC42357 (warning) |
Some overloads here take an Async Function rather than an Async Sub. |
|
BC42105 (warning) |
Function 'NeedsValue' doesn’t return a value on all code paths. |
An |
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 |
|
|
Entry point |
|
|
|
BC36943 |
allowed (C# 6) |
|
BC36943 |
compile error CS1996 |
Async return types |
|
also |
Async iterators, |
none ( |
C# 8 |
|
BC36926 |
CS1988 |
Async lambda type |
anonymous delegate with |
|
Awaiting a |
BC30491 |
CS4008 |
Not awaited call |
warning BC42358 |
warning CS4014 |
|
|
|
Extension |
works |
works |
|
unwraps the inner task |
unwraps the inner task |
Parentheses around |
|
|
Older material you may meet
-
Visual Basic 2010 and earlier used the Begin/End asynchronous pattern,
BackgroundWorkerand raw threads. They still compile, butAsync/Awaitreplaces them (see the threads page forBackgroundWorker). DelegateBeginInvokeandEndInvokethrowPlatformNotSupportedExceptionon .NET 5 and later. -
Books and blog posts that show
Async Sub MainorAsync Function Mainfor console programs do not compile with any Visual Basic version (BC36934, BC30737): use theGetAwaiter().GetResult()bridge. -
Samples that fetch pages with the older HTTP classes belong to the .NET Framework era; the
HttpClientcode here is the current form. Samples that create a newHttpClientper request in aUsingblock, as the examples here do for brevity, are a known source of socket exhaustion in services. -
Code from the Framework era that calls
.Resultor.Wait()inside a UI or ASP.NET event handler works only by luck; it is the deadlock shown above. Make the handlerAsync. -
Some tutorials for real-time apps (SignalR chat samples, for instance) show console clients with
Async Sub Main; the equivalent is theSub Mainbridge withRunAsync().GetAwaiter().GetResult().
Pitfalls checklist
| Pitfall | What to do |
|---|---|
|
Use |
Blocking on a task ( |
Make the caller |
Forgetting |
Treat the warning as an error. The call runs, the result and failure are lost. |
|
Capture, leave the block, then await. |
Holding a |
Use |
Throwing exceptions in a method named |
Decide whether validation throws synchronously or via the task, and be consistent. |
|
Keep the |
A new |
Reuse one, or use |
No time-out or token on network calls |
Pass a |
|
Remove it; |
Capturing the loop variable of |
Copy it to a local first (BC42324). |
Unobserved exceptions in tasks that were never awaited |
Await, store or |
|
Declare |
Calling |
Await, or check |
References
-
Asynchronous programming (Visual Basic), Walkthrough: Accessing the Web by using Async and Await and Async return types
-
Async and Await Operator
-
Cancel an async task or a list of tasks, Cancel async tasks after a period of time and Start multiple async tasks and process them as they complete
-
Fine-tuning your async application, Handling reentrancy in async apps and Control flow in async programs
-
Task-based asynchronous pattern (TAP) and Asynchronous programming patterns
-
Task, Task(Of TResult), ValueTask(Of TResult), TaskCompletionSource(Of TResult), SynchronizationContext and Progress(Of T)
-
HttpClient, Guidelines for using HttpClient and IHttpClientFactory
-
Asynchronous programming (C#) for the C# side