Swift Concurrency (Part 01) — Convert a callback function into an Async function using Continuation.

Habibur Rahman Ovie
2 min readNov 26, 2023

--

Callbacks are good but messy. Async and await replace callbacks with a linear syntax for performing concurrent computations. This makes the code more readable and clean. This article will demonstrate how to convert a callback function into an async function.

Let's consider you are using a 3rd party library that has callback functions but you don’t want to use those callbacks because callbacks make your code smelly. This article will help you to make a wrapper async function on top of a callback function.

private func delayAfter(delay: Int, completation: @escaping ((Int) -> Void)) {
let dispatchAfter = DispatchTimeInterval.seconds(delay)
DispatchQueue.main.asyncAfter(deadline: .now() + dispatchAfter) {
completation(delay)
}
}

For simplicity, we consider the above callback function which delays a certain time and after that delay, it returns the delay time in the callback. To convert this one into an async function we use continuation.

private func delayAfterAsync(delay: Int) async -> Int {
return await withCheckedContinuation({ continuation in
delayAfter(delay: delay) { delay in
continuation.resume(returning: delay)
}
})
}

This delayAfterAsync is an async function which is a wrapper of our previous callback function. Here we use withCheckedContinuation function where we pass our delayAfter callback function.
You must resume the continuation exactly one time, not twice or zero times.
If you call the checked continuation twice or more, Swift will cause your program to halt. It will just crash.
On the other hand, if you fail to resume the continuation at all, Swift will print out a large warning in your debug log, because you’re leaving the task suspended, causing any resources it’s using to be held indefinitely.

// Calling the async function
Task { await delayAfterAsync(delay: 2) }

We can now easily call the async function using the above code.

The above example did not throw any error but your callback may throw an error. So let’s make a throwable async function.

private func delayAfterAsyncThrowable(delay: Int) async throws -> Int {
return try await withCheckedThrowingContinuation({ continuation in
delayAfter(delay: delay) { _ in
if delay % 2 == 0 {
continuation.resume(returning: delay)
} else {
continuation.resume(throwing: CustomError.delayError)
}
}
})
}

//Custom Erroe
enum CustomError: Error {
case delayError
}

For simplicity instead of modifying the delayAfter function, we just put the error handling part inside withCheckedThrowingContinuation function. If the delay is even then we consider this as success otherwise it throws an error.

Task{

do {
let delay = try await delayAfterAsyncThrowable(delay: 2)
print("02 Returning After \(delay)")
}catch{
print("02 error - \(error)")
}

}

As this is throwable, we use do catch block to call this function.

Check the full code here.
Contact me on Linkedin.

--

--

Habibur Rahman Ovie
Habibur Rahman Ovie

No responses yet