It was brought to my attention the other day at work that Swift's JSONSerialization.jsonObject method was throwing runtime errors inside a do catch statement of type AnyObject and the do catch statement was not catching those errors as expected. The obvious fix to prevent this issue from occurring was a logical mistake that we had made, but it made me think deeper into why the fatal runtime error that was being thrown was not caught by the do catch statement? At first thought, I figured that this was a result of a general logic error in the catch statement and we were not catching the proper error that was bubbling up to be caught correctly in the do catch statement. This led me to dig deeper into the issue. So I decided to do a bit of research and attempt to reproduce the issue in Xcode 8 using Swift 3.0. Sure enough, the issue showed up again there too?
Below I have included a screen shot of the runtime issue to illustrate the crash live in Xcode. In the code we are sending a string URL to a function and asking that function to cast that URL as a NSURL type. After that, we could properly use an if let statement to ensure that the method NSData(contentsOf: locationURL as URL) is properly setting the NSData before it is used in the jsonObject method, but I have commented this out to illustrate the runtime crash not being caught properly. Next, I create a do catch statement that will catch an AnyObject thrown as a result of a runtime error on a method call as described in the documentation for jsonObject. To make sure that the nothing is set to the weatherData variable I optimistically set the weatherData variable with NSData(contentsOf: locationURL as URL) but not if let statement to validate the contents of the variable. Next, to actually create the runtime crash I use the weatherData variable and run the jsonObject method call on it using the try keyword thinking that this AnyObject error thrown would then be caught by the catch statement but it is not for some reason?
The code I added below is a brief snippet that illustrates this bug. This code is a recreation of our original error but reproduced in Xcode8 using Swift 3. The purpose is to load an intentional bad URL and have the weatherData variable not be set when the jsonObject is run with the try keyword. This in turn should throw the AnyObject error that should be caught with the catch statement.
func getWeatherInfo(fromCurrentURL URLString: String){ // URLString = "https://www.badurl.url" guard let locationURL = NSURL(string: currentURL) else { print("Unable to form location URL") return } // To ensure weather data is set correctly use an if let statement to validate that NSData is set before using it //if let weatherData = NSData(contentsOf: locationURL as URL) { do { // Here we are optimistically assuming that weather data is present and assigning it to the variable let weatherData = NSData(contentsOf: locationURL as URL) // Because weatherData is optimistically set above and we guarantee that is is present in the jsonObject // call using the as! Data statement then the try statement throws a fatal runtime error and it is // not caught in the do catch block. Why is it not caught in the do catch block? let json = try JSONSerialization.jsonObject(with: weatherData as! Data, options: JSONSerialization.ReadingOptions.mutableContainers) if let currentDict = json as? NSDictionary { if let currentArray = currentDict["currently"] as? NSDictionary { // Data is parsed here }else { print("Unable to deserialze") } } }catch let error as NSError { print("NSError CAUGHT ERROR \(error) -- Never executed") } catch { print("Catch Any Other Errors -- Never executed") } //} }
Judging by my research into the API documentation and because I could never catch the error that was thrown by the jsonObject method call I figured that this issue was worthy of a bugs.swift.org ticket. Follow the status of my bug on Swift's Jira project here: https://bugs.swift.org/browse/SR-1975.
Also, if you have any feedback about this bug or have any suggestions for me I am all ears! There maybe something that I missed or failed to see. Any comments are welcomed. Thanks!
** Update July 5th **
I did receive a response from a Apple Engineer on this issue. Here is the response I received:
You're not even getting to the JSON deserialization. Your NSData instance is nil, and the as! cast is unwrapping it without checking. That's a programmer error rather than a recoverable run-time error.
OK, so fair enough, the run-time crash is coming from the force-unwrap that is failing instead of the jsonObject method call. That makes sense from the perspective of why the do catch statement does not catch the AnyObject thrown from the jsonObject method, but it does leave me to wonder why the do catch statement does not catch the force unwrap of a nil.
Digging through the Swift documentation I came across some Swift documentation that states if you try and force unwrap a non-existent value that is will produce a run-time error (included as a screen shot below), but the larger issue again to me is I intended this force unwrap of a nil to occur. Why does the do catch statement not catch this issue?
In closing I guess the only two options are to ensure that the value is preset by using the if let statement for the weatherData constant, as I had previously used, or use a gaurd statement to return or not execute jsonObject if weatherData has no value.
In closing I guess the only two options are to ensure that the value is preset by using the if let statement for the weatherData constant, as I had previously used, or use a gaurd statement to return or not execute jsonObject if weatherData has no value.
Update: (2017-08-13): The following post actually illustrates an example of the safety measures that have been implemented in Swift and not really an issue with Swift. The following post is an example of how using Swift in this way will get you into trouble and how optionals provide type safety by not allowing you to use nil like Objective-C used to do. Instead you need to safely unwrap optionals and avoid referencing the nil value.
// Direct code execution away from the code below if there is not value associated with the weatherData value guard let weatherData = NSData(contentsOf: locationURL as URL) else { print ("run away") return } // The original solution I was using to make sure that weatherData is present. if let weatherData = NSData(contentsOf: locationURL as URL) { do { let json = try JSONSerialization.jsonObject(with: weatherData as Data, options: JSONSerialization.ReadingOptions.mutableContainers) } catch { print("Catch error") } }