Async Programming
Keep your UI smooth and responsive while loading data, reading files, or calling APIs — using Async/Await, Task(Of T), progress reporting, and cancellation.
Async Sub that Awaits a Task. The Await keyword yields the UI thread while waiting, so your form stays responsive — no frozen window, no spinning cursor. Mark event handlers Async Sub and all helper functions Async Function … As Task(Of T). Use IProgress(Of Integer) to report progress back to the UI thread safely, and CancellationTokenSource to let the user cancel a long operation.
34.1 The Problem: Frozen UI
When you call a slow operation synchronously on the UI thread, the form freezes — it cannot repaint, respond to clicks, or even show a progress bar. The fix is to move the slow work off the UI thread using Async/Await.
' ✗ WRONG — blocks the UI thread for 3 seconds Private Sub btnLoad_Click(...) Handles btnLoad.Click lblStatus.Text = "Loading..." ' never actually shows! Thread.Sleep(3000) ' UI frozen for 3s lblStatus.Text = "Done" End Sub ' ✓ CORRECT — UI thread free during Await Private Async Sub btnLoad_Click(...) Handles btnLoad.Click btnLoad.Enabled = False ' prevent double-click lblStatus.Text = "Loading..." ' shows immediately Try Dim students = Await LoadStudentsAsync() dgv.DataSource = students lblStatus.Text = $"Loaded {students.Count} records" Catch ex As Exception lblStatus.Text = $"Error: {ex.Message}" Finally btnLoad.Enabled = True ' always re-enable End Try End Sub ' The async work function — runs off UI thread Private Async Function LoadStudentsAsync() As Task(Of List(Of Student)) Return Await Task.Run(Function() Using conn As New SqliteConnection(CONN_STR) conn.Open() Dim da As New SqliteDataAdapter("SELECT * FROM Students", conn) Dim dt As New DataTable() da.Fill(dt) Return dt.AsEnumerable() _ .Select(Function(r) New Student With { .Name = r("Name").ToString(), .Grade = CDbl(r("Grade")) }).ToList() End Using End Function) End Function
34.2 Progress Reporting with IProgress(Of T)
IProgress(Of T) is the standard way to report progress from a background task back to the UI thread. The Progress(Of T) constructor takes a callback — VB automatically marshals it back to the UI thread so you can safely update controls.
' --- Event handler --- Private Async Sub btnImport_Click(...) Handles btnImport.Click pbImport.Minimum = 0 pbImport.Maximum = 100 pbImport.Value = 0 btnImport.Enabled = False ' Progress(Of Integer) marshals to UI thread automatically Dim progress = New Progress(Of Integer)(Sub(pct) pbImport.Value = pct lblProgress.Text = $"{pct}% complete" End Sub) Await ImportLargeFileAsync("C:\data\students.csv", progress) btnImport.Enabled = True lblProgress.Text = "Import complete!" End Sub ' --- Background work with progress --- Private Async Function ImportLargeFileAsync( path As String, progress As IProgress(Of Integer)) As Task Await Task.Run(Sub() Dim lines = IO.File.ReadAllLines(path) Dim total = lines.Length For i = 0 To total - 1 ' process line i… Thread.Sleep(5) ' simulate work progress.Report(CInt((i + 1) * 100 \ total)) Next End Sub) End Function
34.3 Cancellation
Pass a CancellationToken to any async method. The user clicks Cancel → you call _cts.Cancel() → the next time the background code checks token.ThrowIfCancellationRequested() it throws OperationCanceledException, which you catch and handle cleanly.
Private _cts As CancellationTokenSource = Nothing Private Async Sub btnStart_Click(...) Handles btnStart.Click _cts = New CancellationTokenSource() btnStart.Enabled = False btnCancel.Enabled = True Try Await ProcessDataAsync(_cts.Token) lblStatus.Text = "Completed!" Catch ex As OperationCanceledException lblStatus.Text = "Cancelled by user." Catch ex As Exception lblStatus.Text = $"Error: {ex.Message}" Finally btnStart.Enabled = True btnCancel.Enabled = False _cts.Dispose() End Try End Sub Private Sub btnCancel_Click(...) Handles btnCancel.Click _cts?.Cancel() ' null-safe cancel End Sub Private Async Function ProcessDataAsync(token As CancellationToken) As Task For i = 1 To 100 token.ThrowIfCancellationRequested() ' check on each iteration Await Task.Delay(50, token) ' cancellable delay Next End Function
- Async void (not Sub) — event handlers must be
Async Sub; all other async methods must beAsync Function … As Taskso callers can Await them and catch exceptions. - Not Awaiting —
LoadDataAsync()withoutAwaitstarts the task but never waits. The method continues immediately and you lose the result. - Blocking with .Result or .Wait() — calling
task.Resulton the UI thread deadlocks. Always useAwait. - Forgetting Try/Catch — exceptions in
Async Subhandlers crash the app silently. Always wrap the body in Try/Catch.
The Sync button simulates a blocking call — notice the UI "freezes" (counter stops). The Async button uses simulated Await Task.Delay — the counter keeps ticking while the operation runs.
Simulate importing a large CSV file asynchronously. Watch IProgress(Of Integer) update the bar from the background. Hit Cancel to trigger OperationCanceledException mid-operation.
34.3 GitHub Copilot — Async Database Load
' Write an async Sub LoadStudentsAsync that loads students from SQLite in a background Task, reports integer progress via IProgress(Of Integer), supports cancellation, and on completion binds the result to dgvStudents on the UI thread.'Private Async Sub btnLoad_Click(...) Handles btnLoad.Click
_cts = New CancellationTokenSource()
btnLoad.Enabled = False
btnCancel.Enabled = True
pbLoad.Value = 0
Dim progress = New Progress(Of Integer)(Sub(pct)
pbLoad.Value = pct
lblStatus.Text = $"Loading... {pct}%"
End Sub)
Try
Dim students = Await LoadFromDbAsync(progress, _cts.Token)
dgvStudents.DataSource = students ' back on UI thread — safe
lblStatus.Text = $"Loaded {students.Count} students."
Catch ex As OperationCanceledException
lblStatus.Text = "Load cancelled."
Catch ex As Exception
lblStatus.Text = $"Error: {ex.Message}"
Finally
btnLoad.Enabled = True
btnCancel.Enabled = False
End Try
End Sub
Private Async Function LoadFromDbAsync(
progress As IProgress(Of Integer),
token As CancellationToken) As Task(Of List(Of Student))
Return Await Task.Run(Function()
token.ThrowIfCancellationRequested()
Dim result As New List(Of Student)
Using conn As New SqliteConnection(CONN_STR) : conn.Open()
Using cmd As New SqliteCommand("SELECT * FROM Students", conn)
Using rdr = cmd.ExecuteReader()
Dim rows = rdr.Cast(Of IDataRecord).ToList()
Dim total = rows.Count
For i = 0 To total - 1
token.ThrowIfCancellationRequested()
result.Add(New Student With {
.Name = rows(i)("Name").ToString(),
.Grade = CDbl(rows(i)("Grade"))
})
progress.Report(CInt((i+1)*100\total))
Next
End Using : End Using : End Using
Return result
End Function, token)
End Function
Lesson Summary
- Mark event handlers Async Sub and helper functions Async Function … As Task(Of T). The
Awaitkeyword yields the current thread while the Task runs, keeping the UI responsive. - Disable the triggering button at the start, re-enable it in a Finally block — this prevents double-clicks and ensures the button is always re-enabled even after exceptions.
- Use Task.Run() to push CPU-bound or blocking I/O work to the thread pool. Use the built-in Async overloads (
ReadAllTextAsync,GetAsync) for I/O-bound work — they don't need Task.Run. - IProgress(Of Integer) with
New Progress(Of Integer)(Sub(p) …)automatically marshals the callback to the UI thread — safe to update controls inside the lambda. - Create a CancellationTokenSource, pass
.Tokento async methods, call.ThrowIfCancellationRequested()on each loop iteration. CatchOperationCanceledExceptionseparately from general exceptions to show "Cancelled" vs "Error". - Never call
.Resultor.Wait()on a task from the UI thread — this deadlocks. Always useAwait.
Exercises
Exercise 34.1 — Async CSV Import with Progress
- Convert the CSV import from Lesson 33 to async:
Async Function ImportCsvAsync(path As String, progress As IProgress(Of Integer), token As CancellationToken) As Task(Of List(Of Student)) - Report progress every 10 rows. Update a ProgressBar and a label showing "Row 45 of 500 (9%)".
- Add a Cancel button wired to
_cts.Cancel(). Show "Import cancelled after N rows" in the status bar. - Copilot challenge: "Extend the import to support Task.WhenAll — simultaneously import two CSV files and merge the results when both complete"
Exercise 34.2 — Async Database Save
- Wrap the student Save button (Lesson 31) in an async pattern: disable Save, show spinner label "Saving…", Await Task.Run with the INSERT/UPDATE, re-enable on completion.
- Add a simulated 500 ms delay (
Await Task.Delay(500, token)) so the saving state is visible during testing. - Copilot challenge: "Add an AutoSave feature: every 30 seconds Await a SaveAllChangesAsync() if _dt.GetChanges() IsNot Nothing, and show 'Auto-saved at HH:mm:ss' in the status bar"
Related Resources
← Lesson 33
File I/O & JSON.
Lesson 35 →
Web API & HttpClient.
MS Docs — Async in VB
Official async/await guide for VB.NET.
MS Docs — Task-based async
Task, Task(Of T), Task.Run patterns.
Featured Books
Visual Basic 2022 Made Easy
Covers async patterns, Task, and background operations in Windows Forms.
View on Amazon →