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

◇◆丶佛笑我妖孽 提交于 2019-12-05 11:13:08

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)")
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!