Best way to handle errors from async closures in Swift 2?

I’m using a lot of async network request (btw any network request in iOS need to by async) and I’m finding way to better handle errors from Apple’s dataTaskWithRequest which not supports throws.

I have code like that:

  • Why is 'throws' not type safe in Swift?
  • Convincing Swift that a function will never return, due to a thrown Exception
  • Forwarding an error in Swift
  • func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ()) {
        let request = NSURLRequest(URL: NSURL(string: "http://google.com")!)
    
        if someData == nil {
            // throw my custom error
        }
    
        let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
            data, response, error in
    
            // here I want to handle Apple's error
        }
        task.resume()
    }
    

    I need to parse my possible custom errors and handle possible connection errors from dataTaskWithRequest. Swift 2 introduced throws, but you can’t throw from Apple’s closure because they have no throw support and running async.

    I see only way to add to my completion block NSError returning, but as I know using NSError is old-style Objective-C way. ErrorType can be used only with throws (afaik).

    What’s the best and most modern method to handle error when using Apple network closures? There is no way no use throws in any async network functions as I understand?

    2 Solutions Collect From Internet About “Best way to handle errors from async closures in Swift 2?”

    there are many ways you can solve this, but i would recommend using a completion block which expects a Result Enum. this would probably be the most ‘Swift’ way.

    the result enum has exactly two states, success and error, which a big advantage to the usual two optional return values (data and error) which lead to 4 possible states.

    enum Result<T> {
        case Success(T)
        case Error(String, Int)
    }
    

    Using the result enum in a completion block finishes the puzzle.

    let InvalidURLCode = 999
    let NoDataCode = 998
    func getFrom(urlString: String, completion:Result<NSData> -> Void) {
        // make sure the URL is valid, if not return custom error
        guard let url = NSURL(string: urlString) else { return completion(.Error("Invalid URL", InvalidURLCode)) }
    
        let request = NSURLRequest(URL: url)
        NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
           // if error returned, extract message and code then pass as Result enum
            guard error == nil else { return completion(.Error(error!.localizedDescription, error!.code)) }
    
            // if no data is returned, return custom error
            guard let data = data else { return completion(.Error("No data returned", NoDataCode)) }
    
            // return success
            completion(.Success(data))
        }.resume()
    }
    

    because the return value is a enum, you should switch off of it.

    getFrom("http://www.google.com") { result in
        switch result {
        case .Success(let data):
            // handle successful data response here
            let responseString = String(data:data, encoding: NSASCIIStringEncoding)
            print("got data: \(responseString)");
        case .Error(let msg, let code):
            // handle error here
            print("Error [\(code)]: \(msg)")
        }
    }
    

    another solution would be to pass two completion blocks, one for success and one for error. something along the lines of:

    func getFrom(urlString: String, successHandler:NSData -> Void, errorHandler:(String, Int) -> Void)
    

    There’s an elegant approach utilising a JavaScript-like Promise library or a Scala-like “Future and Promise” library.

    Using Scala-style futures and promises, it may look as follows:

    Your original function

    func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ())

    may be implemented as shown below. It also shows, how to create a promise, return early with a failed future and how to fulfill/reject a promise:

    func sendRequest(someData: MyCustomClass) -> Future<NSData> {
      guard let url = ... else {
        return Future.failure(MySessionError.InvalidURL)  // bail out early with a completed future
      }
      let request = ... // setup request
      let promise = Promise<NSData>()  
      NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
        guard let error = error else { 
          promise.reject(error) // Client error
        }
        // The following assertions should be true, unless error != nil
        assert(data != nil) 
        assert(response != nil)
    
        // We expect HTTP protocol:
        guard let response = response! as NSHTTPURLResponse else {
          promise.reject(MySessionError.ProtocolError)  // signal that we expected HTTP.
        }
    
        // Check status code:
        guard myValidStatusCodeArray.contains(response.statusCode) else {
          let message: String? = ... // convert the response data to a string, if any and if possible
          promise.reject(MySessionError.InvalidStatusCode(statusCode: response.statusCode, message: message ?? ""))
        }
    
        // Check MIME type if given:
        if let mimeType = response.MIMEType {
          guard myValidMIMETypesArray.contains(mimeType) else {
            promise.reject(MySessionError.MIMETypeNotAccepted(mimeType: mimeType))
          }
        } else {
          // If we require a MIMEType - reject the promise.
        }
        // transform data to some other object if desired, can be done in a later, too. 
    
        promise.fulfill(data!)
      }.resume()
    
      return promise.future!
    }
    

    You might expect a JSON as response – if the request succeeds.

    Now, you could use it as follows:

    sendRequest(myObject).map { data in 
      return try NSJSONSerialization.dataWithJSONObject(data, options: [])
    }
    .map { object in
       // the object returned from the step above, unless it failed.
       // Now, "process" the object: 
       ...
       // You may throw an error if something goes wrong:
       if failed {
           throw MyError.Failed
       }
    }
    .onFailure { error in
       // We reach here IFF an error occurred in any of the 
       // previous tasks.
       // error is of type ErrorType.
       print("Error: \(error)")
    }