The difference between checked and unsafe continuations in Swift

Published on: April 24, 2022

When you’re writing a conversion layer to transform your callback based code into code that supports async/await in Swift, you’ll typically find yourself using continuations. A continuation is a closure that you can call with the result of your asynchronous work. You have the option to pass it the output of your work, an object that conforms to Error, or you can pass it a Result.

In this post, I won’t go in-depth on showing you how to convert your callback based code to async/await (you can refer to this post if you’re interested in learning more). Instead, I’d like to explain the difference between a checked and unsafe continuation in this short post.

If you’ve worked with continuations before, you may have noticed that there are four methods that you can use to create a continuation:

  • withCheckedThrowingContinuation
  • withCheckedContinuation
  • withUnsafeThrowingContinuation
  • withUnsafeContinuation

The main thing that should stand out here is that you have the option of creating a “checked” continuation or an “unsafe” continuation. Your gut might tell you to always use the checked version because the unsafe one sounds... well... unsafe.

To figure out whether this is correct, let’s take a look at what we get with a checked continuation first.

Understanding what a checked continuation does

A checked continuation in Swift is a continuation closure that you can call with the outcome of a traditional asynchronous function that doesn’t yet use async/await, typically one with a callback closure.

This might look a bit as follows:

func validToken(_ completion: @escaping (Result<Token, Error>) -> Void) {
  // eventually calls the completion closure
}

func validTokenFromCompletion() async throws -> Token {
    return try await withCheckedThrowingContinuation { continuation in
        validToken { result in
            continuation.resume(with: result)
        }
    }
}

The code above is a very simple example of bridging the traditional validToken method into the async/await world with a continuation.

There are a couple of rules for using a continuation that you need to keep in mind:

  • You should only call the continuation’s resume once. No more, no less. Calling the resume function twice is a developer error and can lead to undefined behavior.
  • You’re responsible for retaining the continuation and calling resume on it to continue your code. Not resuming your continuation means that withCheckedThrowingContinuation will never throw an error or return a value. In other words, your code will be await-ing forever.

If you fail to do either of the two points above, that’s a developer mistake and you should fix that. Luckily, a checked continuation performs some checks to ensure that:

  • You only call resume once
  • The continuation passed to you is retained in your closure

If either of these checks fail, your app will crash with a descriptive error message to tell you what’s wrong.

Of course, there’s some overhead in performing these checks (although this overhead isn’t enormous). To get rid of this overhead, we can make use of an unsafe continuation.

Is it important that you get rid of this overhead? No, in by far the most situations I highly doubt that the overhead of checked continuations is noticeable in your apps. That said, if you do find a reason to get rid of your checked continuation in favor of an unsafe one, it’s important that you understand what an unsafe continuation does exactly.

Understanding what an unsafe continuation does

In short, an unsafe continuation works in the exact same way as a checked one, with the same rules, except it doesn’t check that you adhere to the rules. This means that mistakes will not be caught early, and you won’t get a clear description of what’s wrong in your crash log.

Instead, an unsafe closure just runs and it might crash or perform other undefined behavior when you break the rules.

That’s really all there is to it for an unsafe continuation, it doesn’t add any functionality, it simply removes all correctness checks that a checked continuation does.

Choosing between a checked and an unsafe continuation

The Swift team recommends that we always make use of checked continuations during development, at least until we’ve verified the correctness of our implementation. Once we know our code is correct and our checked continuation doesn’t throw up any warnings at runtime, it’s safe (enough) to switch to an unsafe continuation if you’d like.

Personally, I prefer to use checked continuations even when I know my implementation is correct. This allows me to continue working on my code, and to make changes, without having to remember to switch back and forth between checked and unsafe continuations.

Of course, there is some overhead involved with checked continuations and if you feel like you might benefit from using an unsafe continuation, you should always profile this first and make sure that this switch is actually providing you with a performance benefit. Making your code less safe based on assumptions is never a great idea. Personally, I have yet to find a reason to favor an unsafe continuation over a checked one.