Meaningful Composite Errors

I really like Swifts error handling system for synchronous code. It allows us to produce errors that have all the information we need attached. We are able to choose to handle errors as they occur or throw them further up the chain, delegating that to another part of our code.

Generally our errors represent a specific failure at a specific point in our code; which is fine… like the rest of our code we try to keep things decoupled. But sometimes it would be nice to get information about not just what went wrong… but how.


Example

Lets have a look at a classic example, creating models from json data.

We will use a very naive Decodable setup to create our models from json.

Nothing too crazy so far?… we can see that the decoding will fail if the provided key doesn’t exist, or if the value we find doesn’t match the one we want. The only thing left is to make our models Decodable.

Perfect, and we’re done! now all we need to do is create an owner from our json.

Great! it’s working. What happens if we get some wrong data?

Our error handling works. We can see that when we tried to access name we were expecting to get back a String but instead got back an NSNumber… but wait, is that name on the Owner object or name on the Pet?

With a small example like this it is easy to debug; but we rarely work with a dataset this small. When we are dealing with hundreds or thousands of model objects that have multiple levels of nesting things can become a little bit harder to dig through.

So what can we do?


Composite Errors

By introducing a composite error we are able to produce something more meaningful to us. This error acts like a container which nests all subsequent errors creating something of a stack trace. This will allow you to better see the path your code took that resulted in the final error.

Here’s what it looks like:

Here we have our CompositeDecodeError with a single .error case. Using generics we can ensure that only Decodable objects are passed to it, along with the actual underlying error.

Finally our build function will attempt to build a Decodable, if it fails the error is wrapped up in our CompositeDecodeError, otherwise we will get back our Decodable object.

We also need to make a very small change on each of our models:

All we needed to do is wrap the existing code in try build(...) to add our new composite behaviour. Now when decoding fails we get an new error.

So what we are actually seeing is 3 nested errors describing the sequence of events that resulted in a typeMismatch. We tried to build a Owner object, then a Pet object and lastly we see our original typeMismatch error from before.

Regardless of how deep your nesting is, this technique will show you the path to the error!


Readability

Taking this a step further we can also improve how these errors read by making all of our errors conform to theLocalizedError protocol.

By adding the errorDescription we can now produce more readable errors:

As you can see, one of the nice things about this technique is that it can be introduced into your existing error handling without much extra work. 👌


Until next time… 👋

Let me know what you think, I’d love to hear your thoughts. If you found this helpful please recommend or retweet!

You can catch me on Twitter and iOS Developers.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.