Skip to main content
Swift’s structured concurrency provides many warnings for situations in which you might be using types and functions that are not thread-safe. In Swift 6, most of these warnings will become errors, so you need to know how to prove to the compiler that your types are safe to use concurrently.

Overview

The primary way to create an Effect in the library is via Effect.run, which takes a @Sendable, asynchronous closure. This restricts the types of closures you can use for your effects.
The closure can only capture Sendable variables that are bound with let. Mutable variables and non-Sendable types are not allowed.
There are two primary scenarios where you’ll encounter this restriction:
  1. Accessing state from within an effect
  2. Accessing a dependency from within an effect

Accessing state in an effect

Reducers execute with a mutable, inout state variable, which cannot be accessed from @Sendable closures.

The problem

@Reducer
struct Feature {
  @ObservableState
  struct State { /* ... */ }
  enum Action { /* ... */ }

  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .buttonTapped:
        return .run { send in
          try await Task.sleep(for: .seconds(1))
          await send(.delayed(state.count))
          // 🛑 Mutable capture of 'inout' parameter 'state' is
          //    not allowed in concurrently-executing code
        }
      }
    }
  }
}

The solution

Explicitly capture the state as an immutable value:
return .run { [state] send in
  try await Task.sleep(for: .seconds(1))
  await send(.delayed(state.count))  // ✅
}
Capturing just the values you need makes the closure more focused and explicit about its dependencies.

Accessing dependencies in an effect

Dependencies can be used from asynchronous and concurrent contexts, so they must be Sendable.

Registering dependencies

When extending DependencyValues, you’ll get warnings if your dependency isn’t Sendable:
extension DependencyValues {
  var factClient: FactClient {
    get { self[FactClient.self] }
    // ⚠️ Type 'FactClient' does not conform to the 'Sendable' protocol
    set { self[FactClient.self] = newValue }
    // ⚠️ Type 'FactClient' does not conform to the 'Sendable' protocol
  }
}

Making dependencies Sendable

To fix this, make sure the interface type only holds onto Sendable data, and annotate closure-based endpoints as @Sendable:
1

Add @Sendable to closures

struct FactClient {
  var fetch: @Sendable (Int) async throws -> String
}
2

Conform to Sendable

If your dependency only contains Sendable properties and @Sendable closures, Swift will automatically infer Sendable conformance. Otherwise, explicitly conform:
struct FactClient: Sendable {
  var fetch: @Sendable (Int) async throws -> String
}
Any closures you use to construct FactClient values must now be @Sendable, which restricts what they can capture.

Common patterns

Passing state to async work

case .loadData:
  return .run { [count = state.count, name = state.name] send in
    let data = await fetchData(count: count, name: name)
    await send(.dataLoaded(data))
  }

Using dependencies in effects

@Reducer
struct Feature {
  @Dependency(\.apiClient) var apiClient
  @Dependency(\.continuousClock) var clock
  
  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .fetchUser:
        return .run { send in
          try await clock.sleep(for: .seconds(1))
          let user = try await apiClient.fetchUser(42)
          await send(.userLoaded(user))
        }
      }
    }
  }
}

Shared state in effects

Shared state is already Sendable, so you can capture it directly:
case .incrementLater:
  return .run { [sharedCount = state.$count] send in
    try await Task.sleep(for: .seconds(1))
    await sharedCount.withLock { $0 += 1 }
  }

Actor isolation

Reducers in TCA are @MainActor isolated by default, which means:

State access is safe

All state mutations happen on the main actor

Effects run elsewhere

.run effects execute in the cooperative thread pool

Send is main actor

Sending actions back always happens on the main actor

Views are synchronized

SwiftUI views observe state on the main actor

Crossing actor boundaries

When effects need to send actions, they automatically hop to the main actor:
return .run { send in
  // Running on background thread
  let data = await heavyComputation()
  
  // Automatically hops to main actor
  await send(.dataLoaded(data))
}

Testing concurrent code

When testing features with concurrency:
1

Use controlled clocks

let store = TestStore(initialState: Feature.State()) {
  Feature()
} withDependencies: {
  $0.continuousClock = ImmediateClock()
}
2

Assert on received actions

await store.send(.fetchUser)
await store.receive(\.userLoaded) {
  $0.user = User(id: 1, name: "Blob")
}
3

Ensure effects complete

The test store automatically ensures all effects finish before the test ends.

Swift 6 considerations

In Swift 6, concurrency warnings become errors. To prepare:
In your package or Xcode project, enable complete concurrency checking to get warnings now:
// Package.swift
swiftSettings: [
  .enableUpcomingFeature("StrictConcurrency")
]
Ensure all dependencies conform to Sendable and use @Sendable closures.
Always use capture lists when accessing state in effects:
return .run { [state] send in  // ✅
  // ...
}
Don’t access global mutable state. Wrap it in dependencies.

Common errors and fixes

// ❌
return .run { send in
  await send(.delayed(state.count))
}

// ✅
return .run { [count = state.count] send in
  await send(.delayed(count))
}

Best practices

Always capture state explicitly

Use capture lists even when it seems unnecessary

Make dependencies Sendable

Annotate all closures with @Sendable

Avoid global state

Wrap globals in dependencies for testability

Use actor isolation wisely

Let TCA handle main actor isolation automatically

Resources

Swift Concurrency

Official Swift concurrency documentation

Dependencies

Learn more about dependency injection in TCA

Testing

Learn how to test concurrent features

Effects

Deep dive into the Effect type

Build docs developers (and LLMs) love