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.
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
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
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?
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.
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
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!
Taking this a step further we can also improve how these errors read by making all of our errors conform to the
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. 👌