Swiftly Awaiting Async Code Using Appwrite
Tags: swift, showdev, opensource, ios
Published: 2022-05-17 16:02:55

Modern application development with Swift involves a lot of asynchronous (or "async") programming using closures and completion handlers, but these APIs are hard to use. This gets particularly problematic when many asynchronous operations are used, error handling is required, or control flow between asynchronous calls is required. Using the new concurrency model makes this a lot more natural and less error-prone.

With the **[Appwrite 0.14 release](https://dev.to/appwrite/announcing-appwrite-014-with-11-cloud-function-runtimes-36f5)**, the **Apple** and **Swift** SDKs have been updated with first-class support for async/await. To understand why this is important, and see how to implement the changes, let's make a comparison between the old and new APIs.

## 🏚️ The Old Way

Async programming with explicit callbacks (also referred to as closures or completion handlers) has many problems. The most obvious issue is that they make code more difficult to read and maintain. They also make it hard to write testable code. Let's take a look at how some of those problems manifest:

### Problem 1: Pyramid of Doom

A sequence of simple asynchronous operations often requires deeply-nested closures:

```swift
func getUserProfileImage(userId: String, completion: (Result<ByteBuffer, AppwriteError>) -> Void) {
    let database = Database(self.client)
    let storage = Storage(self.client)
    
    database.getDocument(collectionId: "profiles", userId: userId) { docResult in
        switch docResult {
        case .success(let document):
            storage.getFileDownload(bucketId: "profiles", fileId: document.data["profileImageId"]) { fileResult in
                switch result {
                case .success(let bytes):
                    completion(.success(bytes))
                }
            }
        }
    }
}
```

This "pyramid of doom" makes it difficult to read and keep track of where the code is running. In addition, having to use a stack of closures leads to many second-order effects that we will discuss next.

### Problem 2: Error Handling

Looking at the above example - we are only handling success cases. What if the user doesn't exist? What if the user has no profile image? Callbacks make error handling difficult and very verbose:

```swift
func getUserProfileImage(userId: String, completion: (Result<ByteBuffer, AppwriteError>) -> Void) {
    let database = Database(self.client)
    let storage = Storage(self.client)
    
    database.getDocument(collectionId: "profiles", userId: userId) { docResult in
        switch docResult {
        case .success(let document):
            storage.getFileDownload(bucketId: "profiles", fileId: document.data["profileImageId"]) { fileResult in
                switch result {
                case .success(let bytes):
                    completion(.success(bytes))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        case .failure(let error):
            completion(docResult)
        }
    }
}
```

### Problem 3: Missing completion calls or return statements

It's easy to bail-out of the asynchronous operation early by simply returning without calling the completion block. When forgotten, this issue can be very hard to debug:

```swift
func getUserProfileImage(userId: String, completion: (Result<ByteBuffer, AppwriteError>) -> Void) {
    let database = Database(self.client)
    let storage = Storage(self.client)
    
    database.getDocument(collectionId: "profiles", userId: userId) { docResult in
        switch docResult {
        case .success(let document):
            // ...
        case .failure(let error):
            return // <- Bail out early
        }
    }
}      
```

Now if you call `getUserProfileImage` and an error occurs, the function will never complete and the call site code will be deadlocked. Even when you do remember to call the block, you can still forget to return after that:

```swift
func getUserProfileImage(userId: String, completion: (Result<ByteBuffer, AppwriteError>) -> Void) {
    let database = Database(self.client)
    let storage = Storage(self.client)
    
    database.getDocument(collectionId: "profiles", userId: userId) { docResult in
        switch docResult {
        case .success(let document):
            // ...
        case .failure(let error):
            completion(.failure(error)) // <- No return, execution continues
        }
        
        doSomethingElse(userId) // <- Executes on error
    }
} 
```

## ✨ The New Way

Asynchronous functions (async/await) allow asynchronous code to be written as if it were synchronous code. This immediately addresses _all_ the problems described above by allowing programmers to make full use of the same language constructs that are available to synchronous code.

### Solution 1: No More Pyramid of Doom

The control flow simply reads from the top down, with no nesting:

```swift
func getUserProfileImage(userId: String) async throws -> ByteBuffer {
    let database = Database(self.client)
    let storage = Storage(self.client)
    let document = try await database.getDocument(
        collectionId: "profiles", 
        userId: userId
    )
    let bytes = try await storage.getFileDownload(
        bucketId: "profiles", 
        fileId: document.data["profileImageId"]
    )
    return bytes
}
```

As you can see, this code is much more readable, concise and easier to reason about, while performing the same operations as before.

### Solution 2: Do/catch error handling

Errors can be handled using the `do/catch` syntax, the same way as synchronous code:

```swift
func getUserProfileImage(userId: String) async throws -> ByteBuffer {
    let database = Database(self.client)
    let storage = Storage(self.client)
    
    do {
        let document = try await database.getDocument(
            collectionId: "profiles", 
            userId: userId
        )
        let bytes = try await storage.getFileDownload(
            bucketId: "profiles", 
            fileId: document.data["profileImageId"]
        )
    } catch {
        throw error
    }
    
    return bytes
}    
```

### Solution 3: Completions and nested returns are gone completely

The solution to the third problem is to now... do nothing. We no longer have to worry about missing completion handler calls or nested returns using async/await code.

## 🔮 Future Possibilities

Async code has the potential to change how we develop apps with Swift. Overall, code will become much more readable and robust, with less effort required to implement (increasingly) common async patterns. I am excited to see what developers can create with async/await.

## 📚 Resources

You can use the following resources to learn more and get help.

- 🚀 [Getting Started for Apple Tutorial](https://appwrite.io/docs/getting-started-for-apple)
- 🚀 [Appwrite Github](https://github.com/appwrite)
- 📜 [Appwrite Docs](https://appwrite.io/docs)
- 💬 [Discord Community](https://appwrite.io/discord)