Swift Catch Pattern that binds the error to a variable
Asked Answered
C

3

18

Using Swift 4.2 and XCode 10

In Swift 4.2, DecodingError is an enumeration. There are (currently) four different cases. I can catch each case separately, and bind variables that I can use to log the error as in the following code ...

do {
    let model = try jsonDecoder.decode(BattleShip.self, from: jsonData!)
    print(model)
} catch DecodingError.dataCorrupted(let context) {
    print(context.debugDescription)
} catch DecodingError.keyNotFound(let key, let context) {
    print("\(key.stringValue) was not found, \(context.debugDescription)")
} catch DecodingError.typeMismatch(let type, let context) {
    print("\(type) was expected, \(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
    print("no value was found for \(type), \(context.debugDescription)")
} catch {
    print("I know not this error")
}

But this is a lot of code to put everywhere I could encounter a decoding error. And, if my do{} block has multiple calls that throw, I may need to handle the errors that those methods call differently. The pattern I am trying to implement looks like this ... where decodingError(error) has all of the messy code above

do {
    let object1 = try decoder.decode(SomeClass.self, from: someData)
    try object2.methodThatThrowsSomeOtherError()
} catch <all decoding errors> {      // this is invalid pseudocode
    MyCentralLogger.log.decodingError(error)
} catch let nonDecodingError {
    MyCentralLogger.log.error(nonDecodingError)
}

I can have a catch pattern like this that seems to satisfy all of the enumeration cases (at least it compiles)

} catch is DecodingError {

but the compiler doesn't seem to autobind the 'error' variable, and I don't see any option like

} catch let decodingError is DecodingError {  // THIS IS NOT VALID

If I just catch all errors, I can easily have a switch in a central method that separates the different decoding error cases appropriately. But I want to be able to avoid sending non-decoding errors into that switch. I can also separate my do{} blocks so that I'm only performing decoding steps in it, but this also makes code messy, particularly if you are decoding multiple messages interspersed with other actions.

Suggestions? Thanks all!

Cay answered 2/1, 2019 at 17:17 Comment(0)
K
35

The syntax used in a catch line is exactly the same pattern syntax used in the case of a switch. If you know how to write a case you know how to write a catch.

So, for example, you complain:

} catch let decodingError is DecodingError {  // THIS IS NOT VALID

Right. But this is valid:

} catch let decodingError as DecodingError { 

Oh what a difference one letter makes.

Krumm answered 2/1, 2019 at 17:22 Comment(4)
Thanks Matt. Unfortunately, I think your solution only reveals another error/assumption of mine. When I use 'as', the compiler now complains that my catch block is not exhaustive. It doesn't allow DecodingError to stand in for all of its enumerations. :(Cay
It does stand for all of its enum cases; the problem is that you might throw some other error. Just like a switch once again, the cases must be exhaustive. That is why you still need your catch or catch let nonDecodingError at the end of your series of catches. You always need a mop-up catch at the end! However, that’s irrelevant; I answered the question you asked.Krumm
Of course you did, and much appreciated. I confused myself when I tested your solution by trying it in a method that didn't declare throws at the top. My other cases had that, so the catch block didn't need to be exhaustive. Thanks again.Cay
The rule is that a do...catch construct usually needs to be exhaustive — that is, it needs a mop-up catch at the end. Only inside a throws function it does not need to be exhaustive, because an error thrown in the do block but not caught by any catch block can pass out of the function to be caught higher up the call chain.Krumm
F
2

This is still a lot more code than desired, but maybe a little bit cleaner:

} catch DecodingError.keyNotFound(_, let context),
        DecodingError.valueNotFound(_, let context),
        DecodingError.typeMismatch(_, let context),
        DecodingError.dataCorrupted(let context) {
    print(context.debugDescription)
    MyCentralLogger.log.decodingError(context.underlyingError)
} catch {
    print(error.localizedDescription)
    MyCentralLogger.log.error(error)
}
Flexuous answered 8/1, 2021 at 17:24 Comment(0)
Q
0

Combining both valid answers and a switch statement, the following will save you some almost identical lines:

do {
    let model = try jsonDecoder.decode(BattleShip.self, from: jsonData!)
    print(model)
} catch let decodingError as DecodingError {
    switch decodingError {
    case .typeMismatch(_, let c), .valueNotFound(_, let c), .keyNotFound(_, let c), .dataCorrupted(let c):
        print(c.debugDescription)
    }
} catch {
    print(error.debugDescription)
}

If, f.i., your decoded data misses the property index: Int, it will print

No value associated with key CodingKeys(stringValue: "index", intValue: nil) ("index").

That should be clear enough for debugging purposes.

Quinquagesima answered 17/2, 2022 at 15:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.