Skip to content

Latest commit

 

History

History
304 lines (225 loc) · 8.14 KB

Troubleshooting.md

File metadata and controls

304 lines (225 loc) · 8.14 KB

Troubleshooting

Compilation errors

99% of compilation issues involving PromiseKit can be addressed or diagnosed by one of the fixes below.

Check your handler

return firstly {
      URLSession.shared.dataTask(.promise, with: url)
}.compactMap {
    JSONSerialization.jsonObject(with: $0.data) as? [String: Any]
}.then { dict in
    User(dict: dict)
}

Swift (unhelpfully) says:

Cannot convert value of type '([String : Any]) -> User' to expected argument type '([String : Any]) -> _'

What’s the real problem? then must return a Promise, and you're trying to return something else. What you really want is map:

return firstly {
      URLSession.shared.dataTask(.promise, with: url)
}.compactMap {
    JSONSerialization.jsonObject(with: $0.data) as? [String: Any]
}.map { dict in
    User(dict: dict)
}

Specify closure parameters and return type

For example:

return firstly {
    foo()
}.then { user in
    //…
    return bar()
}

This code may compile if you specify the type of user:

return firstly {
    foo()
}.then { (user: User) in
    //…
    return bar()
}

If it still doesn't compile, perhaps you need to specify the return type, too:

return firstly {
    foo()
}.then { (user: User) -> Promise<Bar> in
    //…
    return bar()
}

We have made great effort to reduce the need for explicit typing in PromiseKit 6, but as with all Swift functions that return a generic type (e.g., Array.map), you may need to explicitly tell Swift what a closure returns if the closure's body is longer than one line.

Tip: Sometimes you can force a one-liner by using semicolons.

Acknowledge all incoming closure parameters

Swift does not permit you to silently ignore a closure's parameters. For example, this code:

func _() -> Promise<Void> {
    return firstly {
        proc.launch(.promise)      // proc: Foundation.Process
    }.then {
        when(fulfilled: p1, p2)    // both p1 & p2 are `Promise<Void>`
    }
}

Fails to compile with the error:

Cannot invoke 'then' with an argument list of type '(() -> _)

What's the problem? Well, Process.launch(.promise) returns Promise<(String, String)>, and we are ignoring this value in our then closure. If we’d referenced $0 or named the parameter, Swift would have been satisfied.

Assuming that we really do want to ignore the argument, the fix is to explicitly acknowledge its existence by assigning it the name "_". That's Swift-ese for "I know there's a value here, but I'm ignoring it."

func _() -> Promise<Void> {
    return firstly {
        proc.launch(.promise)
    }.then { _ in
        when(fulfilled: p1, p2)
    }
}

In this situation, you won't always receive an error message that's as clear as the one shown above. Sometimes, a missing closure parameter sends Swift scurrying off into type inference limbo. When it finally concludes that there's no way for it to make all the inferred types work together, it may end up assigning blame to some other closure entirely and giving you an error message that makes no sense at all.

When faced with this kind of enigmatic complaint, a good rule of thumb is to double-check your argument and return types carefully. If everything looks OK, temporarily add explicit type information as shown above, just to rule out misinference as a possible cause.

Try moving code to a temporary inline function

Try taking the code out of a closure and putting it in a standalone function. Now Swift will give you the real error message. For example:

func doStuff() {
    firstly {
        foo()
    }.then {
        let bar = bar()
        let baz = baz()
        when(fulfilled: bar, baz)
    }
}

Becomes:

func doStuff() {
    func fluff() -> Promise<…> {
        let bar = bar()
        let baz = baz()
        when(fulfilled: bar, baz)  
    }

    firstly {
        foo()
    }.then {
        fluff()
    }
}

An inline function like this is all you need. Here, the problem is that you forgot to mark the last line of the closure with an explicit return. It's required here because the closure is longer than one line.

You copied code off the Internet that doesn’t work

Swift has changed a lot over the years and so PromiseKit has had to change to keep up. The code you copied is probably for an older PromiseKit. Read the definitions of the functions. It's easy to do this in Xcode by option-clicking or command-clicking function names. All PromiseKit functions are documented and provide examples.

"Context type for closure argument expects 1 argument, which cannot be implicitly ignored"

You have a then; you want a done.

"Missing argument for parameter #1 in call"

This is part of Swift 4’s “tuplegate”.

You must specify your Void parameter:

seal.fulfill(())

Yes: we hope they revert this change in Swift 5 too.

"Ambiguous reference to 'firstly(execute:)'"

Remove the firstly, e.g.:

firstly {
    foo()
}.then {
    //…
}

becomes:

foo().then {
    //…
}

Rebuild and Swift should now tell you the real error.

Other issues

Pending Promise Deallocated!

If you see this warning, you have a path in your Promise initializer that allows the promise to escape without being sealed:

Promise<String> { seal in
    task { value, error in
        if let value = value as? String {
            seal.fulfill(value)
        } else if let error = error {
            seal.reject(error)
        }
    }
}

There are two missing paths here, and if either occurs, the promise will soon be deallocated without resolving. This will manifest itself as a bug in your app, probably the awful infinite spinner.

So let’s be thorough:

Promise<String> { seal in
    task { value, error in
        if let value = value as? String {
            fulfill(value)
        } else if let error = error {
            reject(error)
        } else if value != nil {
            reject(MyError.valueNotString)
        } else {
            // should never happen, but we have an `PMKError` for task being called with `nil`, `nil`
            reject(PMKError.invalidCallingConvention)
        }
    }
}

If this seems tedious, it shouldn’t. You would have to be this thorough without promises, too. The difference is that without promises, you wouldn’t get a warning in the console notifying you of your mistake!

Slow compilation / compiler cannot solve in reasonable time

Add return types to your closures.

My promise never resolves

There are several potential causes:

1. Check to be sure that your asynchronous task even starts

You’d be surprised how often this is the cause.

For example, if you are using URLSession without our extension (but don’t do that; use our extension! we know all the pitfalls), did you forget to call resume on the task? If so, the task never actually starts, and so of course it never finishes, either.

2. Check that all paths in your custom Promise initializers are handled

See “Pending Promise Deallocated” above. Usually you will see this warning if you are not handling a path, but that requires your promise deallocate, so you may not see this warning yet you are still not handling all paths.

Unhandled paths mean the promise will not resolve.

3. Ensure the queue your promise handler runs upon is not blocked

If the thread is blocked the handlers cannot execute. Commonly you can see this if you are using our wait() function. Please read the documentation for wait() for suggestions and caveats.

4. Your promise returned a cancellation error

Cancelation is neither success nor failure. So this is the correct behavior. Use a finally if you need to do some clean up.

Result of call to 'done(on:_:)' is unused, Result of call to 'then(on:_:)' is unused

PromiseKit deliberately avoids the @discardableResult annotation because the unused result warning is a hint that you have not handled the error in your chain. So do one of these:

  1. Add a catch
  2. return the promise (thus punting the error handling to the caller)
  3. Use cauterize() to silence the warning.

Obviously, do 1 or 2 in preference to 3.