https://github.com/kylehughes/swift-async-result
Swift's Result type, extended with an in-progress case for asynchronous operations.
https://github.com/kylehughes/swift-async-result
asynchronous-programming structured-concurrency swift
Last synced: 3 months ago
JSON representation
Swift's Result type, extended with an in-progress case for asynchronous operations.
- Host: GitHub
- URL: https://github.com/kylehughes/swift-async-result
- Owner: kylehughes
- Created: 2026-03-30T03:36:32.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-05T07:17:30.000Z (3 months ago)
- Last Synced: 2026-04-05T09:10:07.240Z (3 months ago)
- Topics: asynchronous-programming, structured-concurrency, swift
- Language: Swift
- Homepage: https://kylehughes.github.io/swift-async-result/
- Size: 18.6 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# AsyncResult
[](https://swiftpackageindex.com/kylehughes/swift-async-result)
[](https://swiftpackageindex.com/kylehughes/swift-async-result)
[](https://github.com/kylehughes/swift-async-result/actions/workflows/test.yml)
`Result`, extended with an in-progress case for asynchronous operations, e.g.
```swift
enum AsyncResult {
case completed(Result)
case inProgress
}
```
## About
AsyncResult adds an `.inProgress` case to `Result` for representing loading, success, and failure states.
AsyncResult has no dependencies. Tests cover 100% of lines and functions.
### Capabilities
* Combinators: `map`, `flatMap`, `mapError`, `flatMapError`, `tryMap`, `merge`, `zip`, `collect`.
* Typed throws support across all throwing APIs.
* `recover` with type-level proof (`AsyncResult`) that recovery occurred.
* `Failure == Never` specialization with `value`, `setFailureType(to:)`.
* Optional interop: `init(optional:or:)`, `unwrap(or:)`.
* Sync and async overloads for all combinators.
* Swift 6 language mode support with strict concurrency.
## Supported Platforms
* iOS 13.0+
* macOS 10.15+
* tvOS 13.0+
* visionOS 1.0+
* watchOS 6.0+
## Requirements
* Swift 6.2+
* Xcode 26.0+
## Documentation
[Documentation is available on GitHub Pages.](https://kylehughes.github.io/swift-async-result/)
## Installation
### Swift Package Manager
```swift
dependencies: [
.package(url: "https://github.com/kylehughes/swift-async-result.git", .upToNextMajor(from: "1.0.0")),
]
```
## Getting Started
`AsyncResult?` serves as view model state, where `nil` means idle:
```swift
import AsyncResult
@Observable
final class UserViewModel {
var user: AsyncResult?
func load() async {
user = .inProgress
user = await AsyncResult { try await api.fetchUser() }
}
}
```
Chain transformations, including throwing ones, on the result:
```swift
let displayName = user
.map(\.profile)
.tryMap { try JSONDecoder().decode(Profile.self, from: $0) }
.map(\.displayName)
```
Recover from errors with type-level proof:
```swift
let safeResult: AsyncResult = user
.map(\.name)
.recover { _ in "Unknown" }
// nil only when in progress; failure is impossible
let name = safeResult.value
```
Combine multiple in-flight requests:
```swift
let combined = profileResult.zip(with: settingsResult)
// or collect a whole array:
let all = AsyncResult.collect(itemResults)
```
## Usage
### State Modeling
`AsyncResult` has two cases: `.inProgress` and `.completed(Result)`. Use `AsyncResult?` where `nil`
represents idle, before any operation has been initiated.
```swift
@State private var result: AsyncResult<[Item], any Error>?
var body: some View {
switch result {
case nil: ContentUnavailableView("Tap to load", ...)
case .inProgress: ProgressView()
case .completed(.success(let items)): ItemListView(items: items)
case .completed(.failure(let error)): ErrorView(error: error)
}
}
```
### Throwing Transforms
`tryMap` transforms the success value with a closure that can fail. It follows the same overload pattern as
`init(catching:)`:
```swift
// Typed throws: the closure throws the Failure type directly
result.tryMap { (data: Data) throws(APIError) -> User in
try decoder.decode(User.self, from: data)
}
// Untyped throws with error mapping
result.tryMap(
{ try JSONDecoder().decode(User.self, from: $0) },
mapError: { _ in .decodingFailed }
)
```
### Combining Results
`merge`, `zip`, and `collect` all use the same priority: failure > inProgress > success.
```swift
// Zip two results into a tuple
let combined = profileResult.zip(with: avatarResult)
// Merge with a custom transform
let summary = nameResult.merge(with: ageResult) { "\($0), \($1)" }
// Collect an array of results
let allItems = AsyncResult.collect(itemResults) // AsyncResult<[Item], any Error>
```
### Recovery and Never
`recover` transforms failures into successes and returns `AsyncResult`:
```swift
let safe = result.recover { _ in fallbackValue }
safe.value // nil only means in-progress
```
`setFailureType(to:)` composes infallible results with fallible ones:
```swift
let infallible = AsyncResult(42)
let fallible = AsyncResult.completed(.success("hello"))
let zipped = infallible.setFailureType(to: MyError.self).zip(with: fallible)
```
### Optional Interop
```swift
// Create from an optional
let result = AsyncResult(optional: cachedUser, or: CacheError.miss)
// Unwrap an optional success value
let unwrapped: AsyncResult = result.unwrap(or: APIError.notFound)
```
## Important Behavior
* `AsyncResult` does not have an idle case. Use `AsyncResult?` where `nil` represents the state before any operation
has been initiated.
* `merge`, `zip`, and `collect` use failure > inProgress > success priority. A failure in any position is never hidden
by an in-progress state elsewhere.
* `recover` returns `AsyncResult`, which proves at the type level that error handling has occurred. The
`value` property on `Never`-failure results returns `nil` only for in-progress, never for failure.
## Contributions
AsyncResult is not accepting source contributions at this time. Bug reports will be considered.
## Author
[Kyle Hughes](https://kylehugh.es)
[![Bluesky][bluesky_image]][bluesky_url]
[![LinkedIn][linkedin_image]][linkedin_url]
[![Mastodon][mastodon_image]][mastodon_url]
[bluesky_image]: https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff
[bluesky_url]: https://bsky.app/profile/kylehugh.es
[linkedin_image]: https://img.shields.io/badge/LinkedIn-0A66C2?logo=linkedin&logoColor=fff
[linkedin_url]: https://www.linkedin.com/in/kyle-hughes
[mastodon_image]: https://img.shields.io/mastodon/follow/109356914477272810?domain=https%3A%2F%2Fmister.computer&style=social
[mastodon_url]: https://mister.computer/@kyle
## License
AsyncResult is available under the MIT license.
See `LICENSE` for details.