Linting & Concurrency
Linting & Concurrency
Section titled “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
Section titled “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
Section titled “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
Section titled “Diagnose why the declaration is async”- Protocol requirement — the protocol method/property is
async. - Override requirement — base class API is
async. @concurrentrequirement — staysasynceven withoutawait.- Accidental/legacy
async— no caller needs async semantics.
Preferred fixes (order)
Section titled “Preferred fixes (order)”- Remove
async(and adjust call sites) when no async semantics are needed. - If
asyncis 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)
Section titled “Suppression examples (keep scope tight)”// swiftlint:disable:next async_without_awaitfunc fetch() async { perform() }
// For a block:// swiftlint:disable async_without_awaitfunc makeMock() async { perform() }// swiftlint:enable async_without_awaitQuick checklist
Section titled “Quick 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
Section titled “Compiler Warnings: Sendable & Isolation”The Swift compiler generates concurrency-related warnings based on strict concurrency checking level.
Common Warning Patterns
Section titled “Common Warning Patterns”“Capture of non-sendable type”
// Warning: Capture of 'self' with non-sendable type 'MyClass' in a `@Sendable` closureTask { 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 calllet 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
Section titled “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 contextfunc 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
Section titled “Suppression Strategies”When to Suppress vs. Fix
Section titled “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
Section titled “Suppression Annotations”// Suppress Sendable warnings for legacy imports@preconcurrency import LegacyFramework
// Suppress for a single declarationnonisolated(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
Section titled “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] = [:]}