Here's a scenario every Xojo developer hits eventually. You have a button. The user clicks it. Your code runs, reads a file, queries a database, calls an API. The operation takes a few seconds. During those seconds, the window goes gray. The spinning beachball appears on macOS. The title bar says "Not Responding" on Windows. The user clicks again, thinking nothing happened. Now it runs twice.

The fix looks simple: run the slow operation in a background thread. And in Xojo, it is simple, until you try to update the UI from that thread, and things start behaving strangely. Controls don't update. You get exceptions you can't explain. Or worse, the app crashes silently and you have no idea why.

Threading in Xojo isn't hard. But it has rules. Break them and you'll spend hours chasing symptoms instead of fixing the actual problem.

A Brief History of Threading in Xojo

Before diving into the mechanics, it's worth understanding how Xojo's threading support has evolved, because the model you're working with today is significantly different from what was available even a few years ago.

Cooperative Threading (REALbasic era, early 2000s). Threading has been part of the language since the REALbasic days. Cooperative threads are the model Xojo has traditionally supported: threads voluntarily yield control at specific points, ensuring most operations are thread-safe. The tradeoff is that only one thread runs at a time, so you don't get true multi-core utilization. It's concurrency without parallelism.

UI Threading improvements (Xojo 2019r2). With 2019r2, updating the UI from a thread became much simpler thanks to the UserInterfaceUpdate event on the Thread class. Before this, communicating between a background thread and the UI required Timer-based workarounds. The new event formalized the pattern and made it significantly cleaner.

Preemptive Threading (Xojo 2024r3, October 2024). This is the big one. Preemptive threads, introduced in 2024r3, allow threads to be interrupted at any point by the scheduler, enabling true multi-core utilization. The new Thread.Type property lets you specify whether a thread is cooperative or preemptive. Corresponding Type properties were also added to Semaphore and CriticalSection, providing distinct locking mechanisms for each threading model.

In short: basic cooperative threading has been around since the early REALbasic days. True multi-core preemptive threading is a much newer addition, arriving in October 2024. Most of the patterns in this post apply to both models, but if you're on 2024r3 or later and using preemptive threads, the data safety considerations become even more important, since preemptive threads can be interrupted mid-operation.

Why the UI Freezes

Xojo applications run on a main thread. The UI, including drawing, event handling, and user input, all happens on that thread. When you run a slow operation directly in a button's Pressed event handler, you're blocking the main thread. Nothing else can happen until your code finishes.

The operating system is still running. The window manager is still trying to service your app. But your app isn't responding to those requests, because the main thread is busy. Hence the beachball. Hence "Not Responding."

The solution is to move slow work off the main thread so the UI stays free to respond.

Xojo's Threading Options

Xojo gives you two primary tools for background work: Thread and Timer. They're different tools for different jobs.

Thread runs code concurrently with the main thread. It's the right choice for genuinely long-running operations, like file processing, network requests you're managing manually, or computation that takes real time. A Thread keeps running until its work is done.

Timer fires a method on the main thread at a set interval. It's not concurrent, it's scheduled. It's the right choice for periodic checks, polling, and lightweight repeated operations. Timers don't block the UI because they run between UI events, not alongside them.

A common mistake is using a Timer with a very short interval to simulate a background thread. This doesn't give you concurrency. It gives you a method that fires frequently on the main thread, which can actually make UI responsiveness worse. Use the right tool.

The One Rule You Cannot Break

Never update the UI directly from a background thread.

This is the rule. It applies without exception. If you access a control from a background Thread, whether you're reading its value, setting its text, changing its visibility, or calling any method on it, you're in undefined behavior territory. Sometimes it works. Sometimes it crashes. Sometimes it corrupts state in ways that are nearly impossible to diagnose.

Xojo's UI is not thread-safe. It was designed to be accessed from the main thread only. This isn't a Xojo quirk, it's the same constraint in almost every UI framework on every platform.

The corollary: all UI updates must happen on the main thread.

The Right Pattern: Thread + Timer

The cleanest approach for background work that needs to update the UI is a Thread paired with a Timer.

The Thread does the work and stores the result in a shared variable or property. The Timer polls periodically on the main thread, checks whether there's a result to display, and updates the UI when there is.

Here's a complete example. It fetches data in a background thread and updates a DesktopTextArea when it's done:

// New API (Xojo 2019r2+)
// Background work with Thread + Timer UI update pattern

// Properties on the Window:
//   WorkerThread As Thread
//   PollTimer As Timer
//   WorkResult As String
//   WorkComplete As Boolean

Sub StartBackgroundWork()
  // Reset state
  WorkResult = ""
  WorkComplete = False
  ResultArea.Text = ""
  StatusLabel.Text = "Working..."

  // Set up the polling timer
  PollTimer = New Timer
  PollTimer.Period = 100  // Check every 100ms
  AddHandler PollTimer.Action, AddressOf OnPollTimer
  PollTimer.RunMode = Timer.RunModes.Multiple

  // Set up and start the thread
  WorkerThread = New Thread
  AddHandler WorkerThread.Run, AddressOf DoBackgroundWork
  WorkerThread.Start
End Sub

Sub DoBackgroundWork()
  // This runs on the background thread
  // DO NOT touch any UI controls here

  Try
    // Simulate a slow operation
    Dim result As String = PerformSlowOperation()
    WorkResult = result   // Store result in shared property
    WorkComplete = True   // Signal completion
  Catch e As RuntimeException
    WorkResult = "Error: " + e.Message
    WorkComplete = True
  End Try
End Sub

Sub OnPollTimer(sender As Timer)
  // This runs on the main thread — safe to update UI

  If WorkComplete Then
    // Stop polling
    sender.RunMode = Timer.RunModes.Off
    RemoveHandler sender.Action, AddressOf OnPollTimer

    // Update the UI — safe here because we're on the main thread
    ResultArea.Text = WorkResult
    StatusLabel.Text = "Done."
  End If
End Sub

The key discipline: DoBackgroundWork never touches a control. It writes to WorkResult and sets WorkComplete. The timer reads those on the main thread and handles all UI updates.

Shared Data and Thread Safety

When a background thread and the main thread both access the same variable, you have a concurrency concern. In the pattern above, WorkResult and WorkComplete are written by the background thread and read by the timer on the main thread.

For simple cases, like a string result or a boolean flag, this works reliably in Xojo because the operations are atomic at the level of the assignment. But for more complex shared state, such as collections, objects with multiple properties, or anything where a partial update would leave things inconsistent, you need to be more careful.

Xojo provides a CriticalSection class for mutual exclusion. When two threads might access the same data simultaneously, use a CriticalSection to ensure only one can proceed at a time:

// New API (Xojo 2019r2+)
// Protecting shared state with CriticalSection

// Property on the class: ResultLock As CriticalSection

// Initialize in Constructor or Open event:
ResultLock = New CriticalSection

// Background thread — writing shared data:
Sub DoBackgroundWork()
  Dim result As String = PerformSlowOperation()

  ResultLock.Enter
  WorkResult = result
  WorkComplete = True
  ResultLock.Leave
End Sub

// Main thread (Timer) — reading shared data:
Sub OnPollTimer(sender As Timer)
  ResultLock.Enter
  Dim done As Boolean = WorkComplete
  Dim result As String = WorkResult
  ResultLock.Leave

  If done Then
    sender.RunMode = Timer.RunModes.Off
    ResultArea.Text = result
    StatusLabel.Text = "Done."
  End If
End Sub

For the simple flag-and-string pattern, the CriticalSection is technically optional. For anything more complex, it's not. I've found it's worth building the habit early.

Canceling a Running Thread

Long-running threads need a cancellation path. A user who clicks "Cancel" while an operation is running shouldn't have to wait for it to finish.

The right approach is a cooperative cancellation flag, a boolean the thread checks periodically:

// New API (Xojo 2019r2+)
// Cooperative thread cancellation

// Property on the Window: CancelRequested As Boolean

Sub DoBackgroundWork()
  For i As Integer = 1 To largeDataSet.Count
    // Check for cancellation at regular intervals
    If CancelRequested Then
      WorkResult = "Cancelled."
      WorkComplete = True
      Return
    End If

    // Process one item
    ProcessItem(largeDataSet(i))
  Next

  WorkResult = "Complete."
  WorkComplete = True
End Sub

Sub CancelButton_Pressed()
  CancelRequested = True
End Sub

The thread doesn't get killed. It checks the flag and exits cleanly. This matters because abruptly terminating a thread that holds resources, like open files, database connections, or network sockets, can leave those resources in a bad state. Cooperative cancellation lets the thread clean up before it exits.

Progress Reporting

A thread that runs silently for thirty seconds feels broken to the user, even when it isn't. Progress feedback makes slow operations feel fast.

Use the same timer-based pattern for progress as for completion:

// New API (Xojo 2019r2+)
// Progress reporting via shared properties

// Properties on Window:
//   WorkProgress As Double  // 0.0 to 1.0
//   WorkComplete As Boolean

Sub DoBackgroundWork()
  Dim total As Integer = largeDataSet.Count
  For i As Integer = 1 To total
    If CancelRequested Then Return

    ProcessItem(largeDataSet(i))
    WorkProgress = i / total  // Update progress — timer will pick this up
  Next

  WorkComplete = True
End Sub

Sub OnPollTimer(sender As Timer)
  // Update progress bar on main thread
  ProgressBar.Value = WorkProgress * 100

  If WorkComplete Then
    sender.RunMode = Timer.RunModes.Off
    StatusLabel.Text = "Done."
  End If
End Sub

The thread updates a Double property. The timer reads it and updates the progress bar. No direct UI access from the thread.

Common Mistakes and What They Look Like

Updating a control directly from a thread. Symptom: intermittent crashes, controls that don't update reliably, or behavior that changes between runs. Fix: use the timer pattern. Never touch UI from a Thread.

Using App.DoEvents to unfreeze the UI. This was a common workaround before proper threading support. It pumps the event loop manually while your code is running on the main thread. It can cause re-entrancy bugs, where the user clicks the button again while your first operation is still running. Use a real Thread instead.

Starting a thread without checking if one is already running. If the user clicks a button twice quickly, you can end up with two threads running the same operation simultaneously. Add a guard:

// New API (Xojo 2019r2+)
Sub StartButton_Pressed()
  If WorkerThread <> Nil And WorkerThread.State = Thread.ThreadStates.Running Then
    Return  // Already running — ignore the click
  End If

  StartBackgroundWork()
End Sub

Forgetting to remove event handlers. If you use AddHandler to wire up thread or timer events, use RemoveHandler when you're done. Failing to do so can cause memory leaks or event handlers that fire after the thread's context is no longer valid.

The Crawl, Walk, Run Path

Crawl: Identify the slow operations in your app, the ones that freeze the UI. Move them into a Thread. Use the timer pattern to signal completion and update the UI. Get the basic flow working.

Walk: Add error handling inside your threads. Add cancellation support. Add progress reporting. Test what happens when operations fail mid-execution.

Run: Protect complex shared state with CriticalSection. Design your data flow so threads work on copies of data when possible, reducing the surface area for shared-state bugs. Build threading patterns as reusable components rather than one-off implementations per window.

The Bottom Line

Threading in Xojo follows one central rule: the UI lives on the main thread, and only the main thread touches it.

Everything else, the thread doing the work, the timer bridging back to the UI, the shared properties passing results across the boundary, is infrastructure in service of that rule.

Get the rule right. Build the pattern once. Apply it consistently.

Your users will never notice the threading. They'll just notice that your app stays responsive. That's exactly the point.