Automatically generate legacy sync interfaces for your async/await code, enabling easy migration to Swift 6 Structured Concurrency with both APIs available.
AwaitlessKit provides Swift macros to automatically generate synchronous wrappers for your async functions, making it easy to call async APIs from existing synchronous code. This helps you adopt async/await without breaking existing APIs or rewriting entire call chains at once.
- Quick Start
- Why AwaitlessKit?
- Installation
- Core Features
- More Examples
- Documentation
- Migration Overview
- License
- Credits
Add the @Awaitless macro to your async functions to automatically generate synchronous wrappers:
import AwaitlessKit
class DataService {
@Awaitless
func fetchUser(id: String) async throws -> User {
// Your async implementation
}
// Generates: @available(*, noasync) func fetchUser(id: String) throws -> User
}
// Use both versions during migration
let service = DataService()
let user1 = try await service.fetchUser(id: "123") // Async version
let user2 = try service.fetchUser(id: "456") // Generated sync versionSee more examples or documentation for more sophisticated cases.
The Problem: Swift's async/await adoption is an "all-or-nothing" proposition. You can't easily call async functions from sync contexts, making incremental migration painful.
The Solution: AwaitlessKit automatically generates synchronous counterparts for your async functions, allowing you to:
- ✅ Migrate to
async/awaitincrementally - ✅ Maintain backward compatibility during transitions
- ✅ Avoid rewriting entire call chains at once
- ✅ Keep existing APIs stable while modernizing internals
⚠️ Important: This library bypasses Swift's concurrency safety mechanisms. It is a migration tool, not a permanent solution.
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/bonkey/AwaitlessKit.git", from: "7.1.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["AwaitlessKit"]
)
]Swift 6.0+ compiler required (available in Xcode 16 and above).
Generates synchronous wrappers for async functions with built-in deprecation controls.
Output variants:
@Awaitless- Generates synchronous throwing functions that can be called directly from non-async contexts@AwaitlessPublisher- Generates CombineAnyPublisherwrappers for reactive programming patterns@AwaitlessCompletion- Generates completion-handler based functions usingResultcallbacks@AwaitlessPromise&@Awaitable- Bidirectional PromiseKit integration (separateAwaitlessKit-PromiseKitproduct)
Concurrency note: Mark service classes and protocols as
Sendable(and classes asfinalwhere possible) when using AwaitlessKit macros.@AwaitlessPublishernow uses a task-backed publisher (notFuture), so cancelling a subscription cancels the underlyingTask.
Automatically generates sync method signatures and optional default implementations for protocols with async methods.
Execute async code blocks synchronously in non-async contexts.
Automatic runtime thread-safe wrappers for nonisolated(unsafe) properties with configurable synchronization strategies.
Direct function for running async code in sync contexts with fine-grained control.
AwaitlessKit provides a flexible configuration system with multiple levels of precedence for customizing generated code behavior.
- Process-Level Defaults via
AwaitlessConfig.setDefaults() - Type-Scoped Configuration via
@AwaitlessConfigmember macro - Method-Level Configuration via
@Awaitlessparameters - Built-in Defaults as fallback
class APIService {
@Awaitless
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: @available(*, noasync) func fetchData() throws -> Data
}@AwaitlessPublisher generates a publisher backed by a dedicated cancellation-aware task publisher. This provides:
- Correct cancellation propagation (cancelling the subscription cancels the underlying
Task) - Memory behavior (no retained promise closure beyond execution)
- Semantic clarity (single-shot mapping of async result to a Combine stream)
- Clear failure typing: non-throwing async ->
AnyPublisher<Output, Never>, throwing async ->AnyPublisher<Output, Error>
Throwing example:
class APIService {
@AwaitlessPublisher
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: func fetchData() -> AnyPublisher<Data, Error>
}Non-throwing example:
class TimeService {
@AwaitlessPublisher
func currentTimestamp() async -> Int {
Int(Date().timeIntervalSince1970)
}
// Generates: func currentTimestamp() -> AnyPublisher<Int, Never>
}Main thread delivery:
class ProfileService {
@AwaitlessPublisher(deliverOn: .main)
func loadProfile(id: String) async throws -> Profile {
// ...
}
// Generated publisher delivers value & completion on DispatchQueue.main
}Under the hood the macro calls an internal factory that uses TaskThrowingPublisher / TaskPublisher (adapted from a Swift Forums discussion on correctly bridging async functions to Combine) to produce the AnyPublisher.
Bidirectional conversion between async/await and PromiseKit with @AwaitlessPromise and @Awaitable:
// Add PromiseKit integration to Package.swift
.product(name: "AwaitlessKit-PromiseKit", package: "AwaitlessKit")Async to Promise conversion:
import AwaitlessKit
import PromiseKit
class NetworkService {
@AwaitlessPromise(prefix: "promise_")
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: func promise_fetchData() -> Promise<Data>
}
// Use with PromiseKit
service.promise_fetchData()
.done { data in print("Success: \(data)") }
.catch { error in print("Error: \(error)") }Promise to async conversion:
class LegacyService {
@Awaitable(prefix: "async_")
func legacyFetchData() -> Promise<Data> {
return URLSession.shared.dataTask(.promise, with: url).map(\.data)
}
// Generates:
// @available(*, deprecated: "PromiseKit support is deprecated; use async function instead")
// func async_legacyFetchData() async throws -> Data
}
// Use with async/await
let data = try await service.async_legacyFetchData()Perfect for gradual migration between PromiseKit and async/await architectures.
class APIService {
@AwaitlessCompletion
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}AwaitlessKit includes comprehensive DocC documentation with detailed guides, examples, and API reference.
- Usage Guide - Quick reference with practical examples and common patterns
- Examples Guide - Comprehensive examples from basic usage to advanced patterns
- PromiseKit Integration - Bidirectional conversion between async/await and PromiseKit
- Configuration System - Four-level configuration hierarchy and customization options
- Migration Guide - Step-by-step migration strategies and best practices
- Macro Implementation - Technical details for extending and contributing to AwaitlessKit
- Quick Reference - Fast lookup for common macro usage patterns and configurations
- Real-world Examples - From simple async functions to complex migration scenarios
- PromiseKit Integration - Complete bidirectional conversion guide with migration strategies
- Configuration Patterns - Process-level, type-scoped, and method-level configuration examples
- Migration Strategies - Progressive deprecation, brownfield conversion, and testing approaches
- Best Practices - Naming conventions, error handling, and testing approaches
- Technical Details - Macro implementation, SwiftSyntax integration, and extension points
The documentation is designed to help you successfully adopt async/await in your projects while maintaining backward compatibility during the transition period.
Implement async functions while maintaining synchronous compatibility for existing callers:
class DataManager {
// Autogenerate noasync version alongside new async function
@Awaitless
func loadData() async throws -> [String] {
// New async implementation
}
}Add deprecation warnings to encourage migration to async versions while still providing the synchronous fallback:
class DataManager {
@Awaitless(.deprecated("Migrate to async version by Q2 2026"))
func loadData() async throws -> [String] {
// Implementation
}
}Force migration by making the synchronous version unavailable, providing clear error messages about required changes:
class DataManager {
@Awaitless(.unavailable("Sync version removed. Use async version only"))
func loadData() async throws -> [String] {
// Implementation
}
}Complete the migration by removing the AwaitlessKit macro entirely, leaving only the pure async implementation:
class DataManager {
func loadData() async throws -> [String] {
// Pure async implementation
}
}MIT License. See LICENSE for details.
- Wade Tregaskis for
Task.noasyncfrom Calling Swift Concurrency async code synchronously in Swift - Zed Editor for its powerful agentic GenAI support
- Anthropic for Claude 3.7 and 4.0 models
Remember: AwaitlessKit is a migration tool, not a permanent solution. Plan your async/await adoption strategy and use this library to smooth the transition.