Async and Await in VB.NET: Keeping Your UI Alive Without Thread Plumbing
VB.NET's Async/Await lets you write non-blocking UI and server code in ordinary top-to-bottom style. Here's how the state machine works, a WinForms example, and the deadlock and Async Sub traps to avoid.
27 Sept 2026, 09:20 UTC

If you've written VB.NET desktop or service code that calls a web API, queries a database, or reads a large file, you've hit the same wall everyone hits: the call takes a second or two, and during that time your WinForms window stops repainting, or your server thread sits idle holding memory. The classic fixes — background threads, Invoke/BeginInvoke marshaling, callback chains — work, but they bury the actual logic under plumbing.
The takeaway: VB.NET's Async/Await keywords give you the responsiveness of asynchronous code while letting you write it in ordinary top-to-bottom style. The compiler does the state-machine plumbing for you. This post covers how it actually works, a worked example, and the two mistakes that bite people hardest.
What Await actually does
Mark a method with the Async modifier, return Task or Task(Of T), and use Await on any awaitable operation. When execution reaches an Await, the method does not block the thread. Instead, the compiler-generated state machine registers a continuation and returns control to the caller immediately. When the awaited task completes, execution resumes on the next line.
Two consequences matter in practice:
- UI apps:
Awaitcaptures the current synchronization context by default, so after the await completes you resume on the UI thread. You can touch controls directly — noInvokeboilerplate. - Server apps: the thread-pool thread is released during the await, so an ASP.NET service can handle far more concurrent requests with the same thread count.
Because task-based asynchrony is a runtime feature, VB code can await anything written in C# — HttpClient, Entity Framework, file I/O — with zero interop friction.
A worked example
Here's a WinForms button handler that fetches a customer name over HTTP without freezing the form. This assumes .NET Framework 4.5+ or any modern .NET target (check <TargetFramework> in your .vbproj if you're unsure — VB6 and VBA are entirely different languages and do not support this).
Private Async Sub FetchButton_Click(sender As Object, e As EventArgs) Handles FetchButton.Click
FetchButton.Enabled = False
StatusLabel.Text = "Loading..."
Try
Dim name As String = Await GetCustomerNameAsync(42)
StatusLabel.Text = "Customer: " & name ' back on the UI thread, safe to touch controls
Catch ex As HttpRequestException
StatusLabel.Text = "Request failed: " & ex.Message
Finally
FetchButton.Enabled = True
End Try
End Sub
Private Async Function GetCustomerNameAsync(id As Integer) As Task(Of String)
Using client As New HttpClient()
Return Await client.GetStringAsync($"https://api.example.com/customers/{id}")
End Using
End FunctionWhile GetStringAsync is in flight, the UI thread is free: the form repaints, other buttons respond, and the user can move the window. When the response arrives, the continuation runs back on the UI thread and updates the label.
To verify the behavior yourself: drop this into a small WinForms project, point the URL at a slow endpoint (or replace the HTTP call with Await Task.Delay(3000)), and drag the window around during the wait. It stays responsive. If you're curious about the machinery, decompile the built assembly with a tool like ILSpy and you'll see the compiler-generated state machine class.
The two mistakes that bite hardest
Blocking with .Result or .Wait()
Calling GetCustomerNameAsync(42).Result from a UI event handler can deadlock: the blocked UI thread is exactly the thread the continuation needs to resume on. The rule is simple — once a call chain is async, keep it async all the way up. If you want to see the deadlock firsthand, replace the Await in the example with .Result in a throwaway test app; the form will freeze permanently.
Async Sub outside event handlers
Async Sub returns nothing awaitable, so callers can't observe completion — and exceptions escape to the synchronization context, which can crash the process. Reserve Async Sub for event handlers like the button click above (the signature is forced on you there). Everywhere else, return Task or Task(Of T) so exceptions flow to whoever awaits you.
A related trap: fire-and-forget tasks that nobody awaits. If you start a task and drop the reference, failures vanish silently. Either await it, or attach explicit error handling.
Trade-offs worth knowing
Async isn't free. Every Await adds state-machine overhead — small, but real. If you're processing thousands of tiny operations in a tight loop, awaiting each one individually can cost more than it saves; batch them or run them synchronously where throughput matters more than responsiveness. Async also doesn't make any single operation faster — it makes threads stop waiting. A two-second HTTP call still takes two seconds.
Where to start
Pick one blocking call in an existing VB.NET project — a database query or HTTP call in a UI handler is ideal — and convert just that path to Async/Await, propagating Task returns up the call chain. Confirm the target framework supports it, test that the UI stays responsive, and grep for any .Result or .Wait() you introduced along the way. That single conversion teaches you 90% of the pattern, and the rest is repetition.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.