Lesson 34 · Async Programming

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.

Key Takeaway: Every button click that does slow work (file I/O, database query, HTTP call) should be an 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.
Async / Await
VB keyword pair
Async marks a method as asynchronous. Await suspends it until the Task completes — without blocking the UI thread.
Task(Of T)
System.Threading.Tasks
Represents a future value. Async Function LoadData() As Task(Of List(Of T)) — caller Awaits it.
Task.Run()
Task factory
Offload CPU-bound work to the thread pool. Await Task.Run(Sub() HeavyCalculation()).
IProgress(Of T)
Interface
Thread-safe progress reporting. Pass New Progress(Of Integer)(Sub(p) progressBar.Value=p) to background work.
CancellationTokenSource
System.Threading
Create a token, pass it to async methods, call .Cancel() from the button. Check token.IsCancellationRequested.
Task.Delay(ms)
Task utility
Async equivalent of Thread.Sleep. Await Task.Delay(1000) — yields for 1 second without blocking.
Task.WhenAll()
Task combinator
Await multiple tasks in parallel. Await Task.WhenAll(t1, t2, t3) — resumes when ALL complete.
ConfigureAwait
Task method
In library code, use Await task.ConfigureAwait(False) to avoid capturing the UI context.

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.

FrozenUI.vb — the problem and fix
' ✗ 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.

Progress.vb — Visual Basic 2026
' --- 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.

Cancellation.vb — Visual Basic 2026
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
Common Async Mistakes
  • Async void (not Sub) — event handlers must be Async Sub; all other async methods must be Async Function … As Task so callers can Await them and catch exceptions.
  • Not AwaitingLoadDataAsync() without Await starts the task but never waits. The method continues immediately and you lose the result.
  • Blocking with .Result or .Wait() — calling task.Result on the UI thread deadlocks. Always use Await.
  • Forgetting Try/Catch — exceptions in Async Sub handlers crash the app silently. Always wrap the body in Try/Catch.
Try It — Simulation 34.1: Frozen UI vs Async/Await

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.

Async vs Sync — UI Thread Demo
UI heartbeat (ticks when UI is free):
0
Operation status:
Ready

Try It — Simulation 34.2: Progress Reporting & Cancellation

Simulate importing a large CSV file asynchronously. Watch IProgress(Of Integer) update the bar from the background. Hit Cancel to trigger OperationCanceledException mid-operation.

IProgress<Integer> + CancellationTokenSource
Task to simulate:
0%
Idle

34.3 GitHub Copilot — Async Database Load

GitHub Copilot — async database load with progress
You typed: ' 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 Await keyword 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 .Token to async methods, call .ThrowIfCancellationRequested() on each loop iteration. Catch OperationCanceledException separately from general exceptions to show "Cancelled" vs "Error".
  • Never call .Result or .Wait() on a task from the UI thread — this deadlocks. Always use Await.

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"

Next: Lesson 35 — Web API & HttpClient

Call REST APIs with HttpClient, deserialise JSON responses, and build a live weather or currency dashboard.

Continue »

Related Resources


Featured Books

Visual Basic 2022 Made Easy

Visual Basic 2022 Made Easy

by Dr. Liew Voon Kiong

Covers async patterns, Task, and background operations in Windows Forms.

View on Amazon →