This page introduces AwaitlessKit: what it is, what problem it addresses, and how its components fit together. It is the entry point for understanding the library before diving into specific APIs or implementation details. For hands-on setup instructions, see Getting Started. For detailed macro reference, see API Reference.
AwaitlessKit is a Swift macro library that generates synchronous (and legacy-style) counterparts for async/await functions, and vice versa. Its primary purpose is to smooth incremental adoption of Swift Structured Concurrency in codebases that cannot migrate all call sites at once.
AwaitlessKit is a migration tool, not a permanent architectural component. The expectation is that macros are removed once a codebase is fully migrated.
Sources: README.md10-12 README.md349-351
Swift's async/await model requires callers to also be async. This creates an "all-or-nothing" propagation problem: introducing a single async function forces every caller up the call chain to become async as well.
AwaitlessKit solves this by automatically generating bridge functions at the boundary between sync and async contexts, so both sides can coexist during a transition period.
| Scenario | Without AwaitlessKit | With AwaitlessKit |
|---|---|---|
| New async impl, legacy callers | Must rewrite all callers | Autogenerate sync wrapper |
| Legacy Publisher API, new async callers | Must rewrite all implementations | Autogenerate async wrapper |
| Gradual rollout with deprecation | Manual boilerplate per method | Controlled via availability parameter |
| PromiseKit codebase migrating to async | Requires full rewrite | @AwaitlessPromise / @AwaitablePromise |
Sources: README.md49-61 README.md82-103
AwaitlessKit provides two complementary families of macros operating in opposite directions.
@Awaitless* Family — async → sync
Attach to an async function to generate a synchronous (or Combine/completion) counterpart.
| Macro | Generated form |
|---|---|
@Awaitless | @available(*, noasync) func … blocking on Awaitless.run |
@AwaitlessPublisher | func … -> AnyPublisher<T, Error/Never> |
@AwaitlessCompletion | func …(completion: @escaping (Result<T, Error>) -> Void) |
@AwaitlessPromise | func … -> Promise<T> (PromiseKit product) |
#awaitless() | Inline expression: calls Awaitless.run at the call site |
@Awaitable* Family — sync → async
Attach to a legacy Publisher- or completion-returning function to generate an async counterpart.
| Macro | Generated form |
|---|---|
@AwaitablePublisher | func … async throws -> T via AnyPublisher.async() |
@AwaitableCompletion | func … async throws -> T via withCheckedThrowingContinuation |
@AwaitablePromise | func … async throws -> T (PromiseKit product) |
Sources: README.md84-105
The following diagram maps the two macro families to the code entities they generate and the runtime facilities they use.
Sources: README.md84-115 Sources/AwaitlessKit/AwaitlessKit.docc/AwaitlessKit.md25-34
AwaitlessKit is distributed as a Swift Package with three public products, two macro compiler plugins, and a shared core type library.
AwaitlessCore defines the shared types used by both macro plugins and the runtime:
| Type | Role |
|---|---|
AwaitlessAvailability | Controls @available annotation on generated functions |
AwaitlessDelivery | Controls which DispatchQueue a publisher delivers on |
AwaitlessSynchronizationStrategy | Controls queue type for @IsolatedSafe |
AwaitlessConfigData | Aggregated configuration value passed through resolution |
AwaitlessableExtensionGeneration | Controls protocol extension generation behavior |
Sources: Package.swift11-86 README.md124-131
Every code-generating macro consults a four-level precedence chain at compile time to determine how the generated function should be named and annotated.
This means a prefix: specified directly on @Awaitless wins over a prefix set on the enclosing class via @AwaitlessConfig, which wins over a process-wide default set via AwaitlessConfig.setDefaults(). For details, see Configuration System.
Sources: README.md124-131 Sources/AwaitlessKit/AwaitlessKit.docc/AwaitlessKit.md37-44
All synchronous wrappers generated by the @Awaitless* family ultimately call Awaitless.run(), which bridges a synchronous caller into the Swift async runtime using a DispatchSemaphore. This is the mechanism that makes it possible to call async functions without an async context.
Deadlock risk:
Awaitless.run()must not be called from within an actor-isolated context. If the calling thread is also the thread the async runtime needs to resume on, a deadlock will occur. This is the primary reason AwaitlessKit is a migration tool and not a long-term solution.
For implementation details, see Runtime Execution Model and Awaitless.run().
Sources: README.md60 Sources/AwaitlessKit/AwaitlessKit.docc/AwaitlessKit.md106-115
AwaitlessKit is designed to be temporary. The intended lifecycle for a given API is:
| Phase | Action | Macro State |
|---|---|---|
| 1 | Write async implementation; generate sync wrapper | @Awaitless (no availability) |
| 2 | Discourage sync callers with a deprecation warning | @Awaitless(.deprecated("…")) |
| 3 | Block sync callers entirely | @Awaitless(.unavailable("…")) |
| 4 | Remove AwaitlessKit; leave only async function | No macro |
For a full description of the strategy, see Migration Strategy.
Sources: README.md285-337 Sources/AwaitlessKit/AwaitlessKit.docc/AwaitlessKit.md106-116
| Requirement | Value |
|---|---|
| Swift compiler | 6.0+ (Xcode 16+) |
| macOS | 14.0+ |
| iOS | 15.0+ |
| tvOS | 13.0+ |
| watchOS | 10.0+ |
| macCatalyst | 14.0+ |
Awaitless.run() timeout variant | macOS/iOS only (not Linux) |
Sources: Package.swift13 README.md78