All skills
jamesrochabrun avatar

/swift-concurrency

@a3824c9

Guide for building, auditing, and refactoring Swift code using modern concurrency patterns (Swift 6+). This skill should be used when working with async/await, Tasks, actors, MainActor, Sendable types, isolation domains, or when migrating legacy callback/Combine code to structured concurrency. Covers Approachable Concurrency settings, isolated parameters, and common pitfalls.

Use this Skill: https://skilld.dev/gh/jamesrochabrun/skills/swift-concurrency

This session only. Nothing lands on disk.

referencescommon-mistakes.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Common Swift Concurrency Mistakes

Critical Mistakes

Thinking async = background

// This STILL blocks the main thread!
@MainActor
func slowFunction() async {
  let result = expensiveCalculation()  // Synchronous work = blocking
  data = result
}

async means "can pause." The actual work still runs wherever it runs. For CPU-heavy work:

// Swift 6.2+
@concurrent
func processData() async -> Result {
  expensiveCalculation()
}

// Or use Task.detached (less preferred)
Task.detached {
  let result = expensiveCalculation()
  await MainActor.run { self.data = result }
}

Blocking the Cooperative Thread Pool

// NEVER do this - risks deadlock
func badIdea() async {
  let semaphore = DispatchSemaphore(value: 0)
  Task {
    await doWork()
    semaphore.signal()
  }
  semaphore.wait()  // Blocks a cooperative thread!
}

Swift's cooperative thread pool has limited threads (equal to CPU core count). Blocking one with DispatchSemaphore, DispatchGroup.wait(), or similar calls can cause deadlocks.

Solution: Stay fully async. Use async let or restructure.

Creating Too Many Actors

// Over-engineered
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }

// Better - most things can live on MainActor
@MainActor
class AppState { }

Use a custom actor only when ALL conditions are met:

  1. You have non-Sendable state
  2. That state is referenced from more than one place
  3. Operations must be atomic
  4. Operations cannot run on MainActor

If you can't justify it, use @MainActor instead.

Non-Sendable Types with Async Methods

// Problematic - only usable from non-isolated contexts
class MyClass {
  private var state = 0

  func someAsyncFunction() async {
    print(state)  // This only works if caller is non-isolated
  }
}

Solution: Use isolated parameters:

class MyClass {
  private var state = 0

  func someAsyncFunction(isolation: isolated (any Actor)? = #isolation) async {
    print(state)  // Works from any isolation context
  }
}

Common Mistakes

Using MainActor.run When You Don't Need It

// Unnecessary
Task {
  let data = await fetchData()
  await MainActor.run {
    self.data = data
  }
}

// Better - just make the function @MainActor
@MainActor
func loadData() async {
  self.data = await fetchData()
}

MainActor.run is rarely the right solution. If you need MainActor isolation, annotate the function with @MainActor instead.

Creating Unnecessary Tasks

// Unnecessary Task creation
func fetchAll() async {
  Task { await fetchUsers() }
  Task { await fetchPosts() }
}

// Better - use structured concurrency
func fetchAll() async {
  async let users = fetchUsers()
  async let posts = fetchPosts()
  await (users, posts)
}

If you're already in an async context, prefer structured concurrency (async let, TaskGroup). Structured concurrency handles cancellation automatically.

Making Everything Sendable

Not everything needs to cross boundaries. If you're adding @unchecked Sendable everywhere, step back and ask if the data actually needs to move between isolation domains.

Better approach: Keep types non-Sendable and use isolated parameters. Non-Sendable types are perfectly thread-safe when used correctly.

Forgetting Cancellation

// Ignores cancellation
func processItems(_ items: [Item]) async {
  for item in items {
    await process(item)  // Continues even if task is cancelled
  }
}

// Better - respects cancellation
func processItems(_ items: [Item]) async throws {
  for item in items {
    try Task.checkCancellation()
    await process(item)
  }
}

Using Task.detached When Task Suffices

// Usually wrong
Task.detached {
  await doWork()
}

// Usually right
Task {
  await doWork()
}

Task.detached doesn't inherit priority, task-local values, or actor context. Regular Task is usually what you want. Use @concurrent for background work.

SwiftUI-Specific Mistakes

Views Not MainActor-Isolated

SwiftUI's isolation model is error-prone. If you see a SwiftUI view that is not MainActor-isolated, it's probably a bug.

// Potentially problematic
struct MyView: View {
  var body: some View { ... }
}

// Safe - use @Observable which is MainActor by default
@Observable
class ViewModel {
  var items: [Item] = []
}

Both UIKit and AppKit enforce whole-type MainActor isolation, so they're easier to use.

Accessing @State from Detached Tasks

struct MyView: View {
  @State private var data: Data?

  var body: some View {
    Button("Load") {
      Task.detached {
        let result = await fetch()
        // ERROR: Cannot access @State from detached task
        self.data = result
      }
    }
  }
}

Solution: Use regular Task (inherits MainActor) or hop back explicitly:

Task.detached {
  let result = await fetch()
  await MainActor.run {
    self.data = result
  }
}

Anti-Patterns to Avoid

Introducing Actors to Fix Compiler Errors

"This is an actor because the compiler told me to"

This is always a mistake. You might never revisit it, but it reinforces the wrong mental model. Understand why the error exists before adding concurrency constructs.

@preconcurrency as a Blanket Solution

@preconcurrency silences warnings but doesn't fix underlying issues. Use it only for genuine interop with pre-Swift 6 code, not to avoid understanding the problem.

Overusing nonisolated(unsafe)

// Dangerous - disables all checking
nonisolated(unsafe) var globalState = 0

This completely opts out of the compiler's protection. Only use when you've proven safety through other means.

The Right Mental Model

  1. Start with MainActor - Most UI code belongs here
  2. Remove isolation only when needed - Less isolation = more flexibility
  3. Add actors only with justification - Document why an actor is necessary
  4. Use non-Sendable types freely - They're thread-safe when used correctly
  5. Prefer structured concurrency - async let and TaskGroup over unstructured Task
  6. Trust the compiler - Errors are telling you something important

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill is a technical guide for Swift developers on modern concurrency patterns. It contains educational documentation and code examples. No security risks were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    6/6 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at a3824c9. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 9 months ago

README badge

README badge for jamesrochabrun/skills/swift-concurrency