Linting & Concurrency
Use this when:
- SwiftLint flags
async_without_awaitor other concurrency-related warnings. - You need to decide whether to suppress, fix, or reconfigure a concurrency lint rule.
Skip this file if:
- The issue is a compiler diagnostic, not a lint rule. Use
actors.md,sendable.md, orthreading.md.
Jump to:
- SwiftLint Concurrency Rules Overview
async_without_awaitRule- Suppression Strategies
SwiftLint Concurrency Rules Overview
SwiftLint provides several rules targeting async/await and concurrency patterns. Understanding when to fix vs. suppress is critical.
| Rule | Default | Purpose |
|---|---|---|
async_without_await | warning | Flags async functions that never await |
unowned_variable_capture | warning | Warns about unowned in closures (risky in async) |
class_delegate_protocol | warning | Ensures delegates are class-bound (AnyObject) |
weak_delegate | warning | Delegates should be weak to avoid retain cycles |
SwiftLint: async_without_await
- Intent: A declaration should not be
asyncif it never awaits. - Never "fix" by inserting fake suspension (e.g.
await Task.yield(),await Task { ... }.value). Those mask the real issue and add meaningless suspension points. - Legit use of
Task.yield(): OK in tests or scheduling control when you truly need a yield; not as a lint workaround.
Diagnose why the declaration is async
1) Protocol requirement — the protocol method/property is async. 2) Override requirement — base class API is async. 3) @concurrent requirement — stays async even without await. 4) Accidental/legacy async — no caller needs async semantics.
Preferred fixes (order)
1) Remove async (and adjust call sites) when no async semantics are needed. 2) If async is required (protocol/override/@concurrent):
- Re-evaluate the upstream API if you own it (can it be non-async?).
- If you cannot change it, keep
asyncand narrowly suppress the rule where appropriate (common for mocks/stubs/overrides).
Suppression examples (keep scope tight)
// swiftlint:disable:next async_without_await
func fetch() async { perform() }
// For a block:
// swiftlint:disable async_without_await
func makeMock() async { perform() }
// swiftlint:enable async_without_awaitQuick checklist
- [ ] Confirm if
asyncis truly required (protocol/override/@concurrent). - [ ] If not required, remove
asyncand update callers. - [ ] If required, prefer localized suppression over dummy awaits.
- [ ] Avoid adding new suspension points without intent.
Compiler Warnings: Sendable & Isolation
The Swift compiler generates concurrency-related warnings based on strict concurrency checking level.
Common Warning Patterns
"Capture of non-sendable type"
// Warning: Capture of 'self' with non-sendable type 'MyClass' in a `@Sendable` closure
Task {
self.doWork() // 'self' is non-Sendable
}Fixes (in order of preference):
- Make the type
Sendableif it's truly thread-safe - Use
@MainActorisolation if it's UI-related - Capture only Sendable values instead of
self - Use
@unchecked Sendablewith documented safety invariant (last resort)
"Non-sendable result returned"
// Warning: Non-sendable type 'MyResult' returned by implicitly async call
let result = await actor.getData() // Returns non-Sendable typeFixes:
- Make the return type Sendable
- Return Sendable projections (IDs, copies of data)
- Keep processing within the actor's isolation
Actor Isolation Warnings
"Main actor-isolated property accessed from non-isolated context"
// Warning: Main actor-isolated property 'title' cannot be referenced from a non-isolated context
func updateTitle() {
viewModel.title = "New" // viewModel is @MainActor
}Fixes:
- Mark the calling function
@MainActor - Use
await MainActor.run { }for one-off access - Reconsider if the property truly needs @MainActor isolation
Suppression Strategies
When to Suppress vs. Fix
Fix when:
- The warning identifies a real data race risk
- The fix is straightforward (add Sendable, adjust isolation)
- The code is new or actively maintained
Suppress when:
- Protocol/inheritance requires the signature
- Third-party code forces the pattern
- Migration is in progress (with tracked ticket)
Suppression Annotations
// Suppress Sendable warnings for legacy imports
@preconcurrency import LegacyFramework
// Suppress for a single declaration
nonisolated(unsafe) var legacyCallback: (() -> Void)?
// Type-level suppression (use sparingly)
struct LegacyWrapper: @unchecked Sendable {
// Document why this is safe
private let lock = NSLock()
private var value: Int
}Documentation Requirements
When using suppression annotations, document:
- Why the suppression is needed
- What invariant makes it safe
- When it can be removed (link to migration ticket)
/// Thread-safe: Internal lock protects all mutations.
/// TODO: Remove @unchecked when migrated to actor (JIRA-1234)
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
}