swiftpromisepromisekit

Code using Promises doesn't execute at all


I've spent the better part of the day banging my head against the wall over this, and I think I finally understand it. Here's the question I wish had existed this morning when I started looking.

For background, I have several years of experience working with C++ and Python, and I've recently started learning Swift for non-iOS development. Everything I will show here seems to behave the same on a MacBook Pro as on my Ubuntu PC. I'm running Swift 5.4, compiling and running with Swift Package Manager in the command line.

I've read through several articles about using Promises in Swift, and it's not adding up. The examples they show make it seem like you can just call firstly, chain together a few .then calls, and then tie it all up with a .done and a .catch, and everything will just work. When I try this, I mostly get a bunch of errors, but sometimes I get lucky enough to have it compile, only to find that nothing is happening when I run it.

Here's an example main.swift file that illustrates my confusion.

import PromiseKit

firstly {
    Promise<String> { seal in
        print("Executing closure")
        seal.fulfill("Hello World!")
    }
}.done { str in
    print(str)
}

When I run this, it just prints Executing closure, even if I add a sleep after it, or if I call .wait() on the result. Why does the firstly block execute but not the done block?

Here's another one. It took me a while just to get it to compile, but it still doesn't do what I expect it to:

import Foundation
import PromiseKit

let url = URL(string: "https://stackoverflow.com/")!

enum SampleError: Error {
    case noResponse
    case badCode(Int)
    case couldNotDecode
}

firstly {
    Promise<Data> { seal in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                return seal.reject(error)
            }

            guard let data = data, let response = response as? HTTPURLResponse else {
                return seal.reject(SampleError.noResponse)
            }

            guard (200..<300).contains(response.statusCode) else {
                return seal.reject(SampleError.badCode(response.statusCode))
            }

            seal.fulfill(data)
        }.resume()
    }
}.then { data -> Promise<String> in
    Promise<String> { seal in
        if let str = String(data: data, encoding: .utf8) {
            seal.fulfill(str)
        } else {
            seal.reject(SampleError.couldNotDecode)
        }
    }
}.done { str in
    print(str)
}.catch { error in
    print("Error: \(error)")
}

I want this to print the HTML contents of the Stack Overflow home page, but instead it prints nothing.

It seems that I have all the setup in place, but the Promises are not running. How to I get them to actually execute?


Solution

  • It seems that the problem here is that all the online articles focus on iOS development, and don't really address Promises in a desktop/server executable. Simple scripts are intended to terminate, whereas mobile apps are designed to run indefinitely.

    If you dig into the Thenable code, you will find that .then, .done, .catch, and so forth have an argument for a DispatchQueue, which, digging deeper, defaults to DispatchQueue.main. Because the top-level code in these examples is utilizing the main thread, the main DispatchQueue never gets an opportunity to perform the work assigned to it. Both of these examples can be easily made to execute the chain of Promises by yielding the main thread to the dispatch system by calling dispatchMain() at the very end.

    However, dispatchMain() never returns, so it's not a good choice for an executable that is intended to run to completion. The better solution is to designate an alternate queue to execute each of theses promises. For a single task, we can use a semaphore in a .finally block to signal that all the work is done. For multiple tasks, I suspect you would need to use a DispatchGroup.

    Here's an updated version of the first example:

    import Foundation
    import PromiseKit
    
    let queue = DispatchQueue.global()
    let semaphore = DispatchSemaphore(value: 0)
    
    firstly {
        Promise<String> { seal in
            print("Executing closure")
            seal.fulfill("Hello World!")
        }
    }.done(on: queue) { str in
        print(str)
    }.catch(on: queue) { error in
        print("Error: \(error)")
    }.finally(on: queue) {
        semaphore.signal()
    }
    
    semaphore.wait()
    

    And here is the working version of the second one:

    import Foundation
    import PromiseKit
    
    let queue = DispatchQueue.global()
    let semaphore = DispatchSemaphore(value: 0)
    
    let url = URL(string: "https://stackoverflow.com/")!
    
    enum SampleError: Error {
        case noResponse
        case badCode(Int)
        case couldNotDecode
    }
    
    firstly {
        Promise<Data> { seal in
            URLSession.shared.dataTask(with: url) { data, response, error in
                if let error = error {
                    return seal.reject(error)
                }
    
                guard let data = data, let response = response as? HTTPURLResponse else {
                    return seal.reject(SampleError.noResponse)
                }
    
                guard (200..<300).contains(response.statusCode) else {
                    return seal.reject(SampleError.badCode(response.statusCode))
                }
    
                seal.fulfill(data)
            }.resume()
        }
    }.then(on: queue) { data -> Promise<String> in
        Promise<String> { seal in
            if let str = String(data: data, encoding: .utf8) {
                seal.fulfill(str)
            } else {
                seal.reject(SampleError.couldNotDecode)
            }
        }
    }.done(on: queue) { str in
        print(str)
    }.catch(on: queue) { error in
        print("Error: \(error)")
    }.finally (on: queue) {
        semaphore.signal()
    }
    
    semaphore.wait()
    

    Interestingly, the firstly block seems to execute synchronously on the main thread. It occurs to me that if you had a lot of tasks to run, it might be best to do as little work as possible inside of the firstly handler, and free up your main thread so it can spin up the tasks on the other threads more quickly.