-1

I have a function that creates JSON data from a dictionary, and have specified that it throws to propagate the error up the stack:

func createBodyDataFrom(dictionary: [String: Any]) throws -> Data {
        let bodyData = try JSONSerialization.data(withJSONObject: dictionary, options: [])

        return bodyData
    }

However when testing using an XCTAssertThrowsError I get a test failure, according to XCode BECAUSE the function threw an exception

func testCreateJSONFrominValidDictionaryThrows() {
        let validDictionary: [String: Any] = [
            "object": NSObject()
        ]
        XCTAssertThrowsError(try testClient.createBodyDataFrom(dictionary: validDictionary))
    }

The failure message from the IDE gives:

XCTAssertThrowsError failed: throwing Invalid type in JSON write (NSObject)

This seems contradictory, but leaving the failure case untested feels incomplete. Any ideas whats going wrong here?

AndyRyan
  • 1,500
  • 12
  • 23

1 Answers1

-1

Let's start by running some equivalent code in Objective-C so that we understand what Cocoa does under the hood here:

NSDictionary* d = @{@"Howdy": [NSObject new]};
NSError* err;
[NSJSONSerialization dataWithJSONObject:d options:0 error:&err];
NSLog(@"%@", err);

If this were the sort of thing that results in an NSError, we would be able to read the value of err and print it to the console. But it isn't, and we can't. We never got to the NSLog statement. Instead, the program crashes unceremoniously:

Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: 'Invalid type in JSON write (NSObject)'
    1   libobjc.A.dylib  0x00007fff50b97b20 objc_exception_throw + 48
    2   Foundation       0x00007fff2580cb68 -[_NSJSONWriter writeRootObject:toStream:options:error:] + 0
    3   Foundation       0x00007fff2580ff03 ___writeJSONObject_block_invoke + 371
[and so on]

That is not an NSError. It is an NSException. They are completely different things, and basically it means our program crashes. It is not a throw-and-catch of an error; it is sudden death.

That is what the documentation warned us would happen. This method, dataWithJSONObject:options:error, does not fail in good order if we supply an object that cannot be turned into JSON. It crashes instead:

If obj will not produce valid JSON, an exception is thrown. This exception is thrown prior to parsing and represents a programming error, not an internal error. You should check whether the input will produce valid JSON before calling this method by using isValidJSONObject:.

(Italics mine.)

So the correct way to discover the issue and prevent the crash was to call isValidJSONObject: first. This method, dataWithJSONObject:options:error, will not catch this sort of problem and return a mere NSError; it will kill the whole program.

And that is what happened to you. Your call to try JSONSerialization.data(withJSONObject:) did not throw. It just crashed. You can see that if you try it by itself in an actual Swift app:

override func viewDidLoad() {
    super.viewDidLoad()
    do {
        try testCreateJSONFrominValidDictionaryThrows()
    } catch {
        print("oops")
    }
}
func createBodyDataFrom(dictionary: [String: Any]) throws -> Data {
    let bodyData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
    return bodyData
}
func testCreateJSONFrominValidDictionaryThrows() throws {
    let validDictionary: [String: Any] = [
        "object": NSObject()
    ]
    try createBodyDataFrom(dictionary: validDictionary)
}

We run the app, and what happens? We do not print "oops". Instead, we get exactly the same crash as in Objective-C.

And that is what happens to your case as well. We never threw because we crashed instead. And the test failure reports that fact. Your testClient crashed out from under you (you might be able to detect this with some twiddling of the Console), and the test itself came back as a failure because we never threw an NSError.


The difference between Cocoa throwing an NSException and returning an NSError is crucial here. Only the latter counts for Swift as throwing an error; the former is a crash. See for example

How can I throw an NSError from a Swift class and catch it in an Objective-C class?

Avoid handling all exceptions until they've reached a global handler

How to catch NSUnknownKeyException in swift 2.2?

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Handling Errors in Swift: In Swift, this method returns a nonoptional result and is marked with the throws keyword to indicate that it throws an error in cases of failure. You call this method in a try expression and handle any errors in the catch clauses of a do statement, as described in Error Handling in The Swift Programming Language and About Imported Cocoa Error Parameters. – AndyRyan Nov 25 '19 at 08:16
  • I don't actually know under what circumstances this method would return an NSError in good order. So I cannot account for the uninformative (perhaps incorrect) documentation on that point. I know only that the circumstances we are testing, namely a value that cannot produce valid JSON, does not return an NSError in good order; it generates an NSException instead; that is what my code (and yours) proves, and that is in fact documented in a rather elliptical way. I wouldn't set too much store by the docs anyway; I make my living by being skeptical about the docs. Run the code I gave, and see. – matt Nov 25 '19 at 19:06