How to prevent SWIFT TASK CONTINUATION MISUSE

Hello,
Third party API I'm using sometimes calls completion callback multiple times causing fatal error in withCheckedThrowingContinuation

Fatal error: SWIFT TASK CONTINUATION MISUSE: clear() tried to resume its continuation more than once, returning ()!

Are there any options for preventing this crash besides fixing third party library?

I tried setting continuation to nil but there is still some space for resuming twice or more:

    func waitForDataStoreReady() async throws {
        return try await withCheckedThrowingContinuation { continuation in
            
            var nillableContinuation: CheckedContinuation<Void, Error>? = continuation
            
            doSomething { result in
                switch result {
                case .success:
                    nillableContinuation?.resume()
                    nillableContinuation = nil
                    
                case .failure(let error):
                    nillableContinuation?.resume(throwing: error)
                    nillableContinuation = nil
                }
            }
        }
    }

Thanks :)

1 Like

You should probably nil out the variable before resuming the continuing, to reduce the window in which a second invocation of doSomething can happen:

switch result {
case .success:
  if let continuation = nillableContinuation {
    nillableContinuation = nil
    continuation.resume()
  }

case .failure(let error):
  if let continuation = nillableContinuation {
    nillableContinuation = nil
    continuation.resume(throwing: error)
  }
}

However, if doSomething can be invoked simultaneously from multiple threads, that still won't avoid race conditions. You'd need to add some additional synchronization around the check of the continuation.

I end up using simple Lock:

    func waitForDataStoreReady() async throws {
        let lock = NSLock()

        return try await withCheckedThrowingContinuation { continuation in

            var nillableContinuation: CheckedContinuation<Void, Error>? = continuation

            doSomething { result in
                lock.lock()
                defer { lock.unlock() }

                switch result {
                case .success:
                    nillableContinuation?.resume()
                    nillableContinuation = nil

                case .failure(let error):
                    nillableContinuation?.resume(throwing: error)
                    nillableContinuation = nil
                }
            }
        }
    }

Sorry to hijack the thread, but I don’t understand what’s wrong with the original code. Is the continuation called multiple times because of re-entrancy? Where? I can see in general how resuming a continuation while the reference is around can result in subsequent invocations but here the reference is inside a function and resumption happens inside a closure so for the life of me I cannot explain how this code fails.

let's imagine function doSomething looks like this:

func doSomething(callback: (Result<Void, Error>) -> Void) {
    callback(.success(()))
    callback(.success(()))
}

in this case everything is ok, first callback will resume continuation and set it to nil, so second one will do nothing. But function doSomething may call this callback concurrently, like so:

func doSomething(callback: @escaping (Result<Void, Error>) -> Void) {
    for _ in 0..<10 {
        DispatchQueue.global().async {
            callback(.success(()))
        }
    }
}

in this case it may happen that two or more execution of callback will resume continuation before any of them set it to nil

Ah, that makes sense. I did not think much about doSomething, so I though the issue was in the surrounding code, and missed the point. Thanks for clarifying!