Clean Code: Error Handling

Ashley Ng
4 min readMay 1, 2018

--

Next chapter in the series is Error Handling. This was a pretty short chapter, but it essentially revolved around throwing exceptions and exception handling. It also mentioned nil values, but Swift is definitely handles nils differently than Java does. There is a link to the playground with some example code at the bottom of this post, and without further ado, here are the take aways from the chapter.

Many code bases are completely dominated by error handling…I don’t mean that error handling is all that they do. I mean that it is nearly impossible to see what the code does because of all the scattered error handling. Error handling is important, but if it obscures logic, it’s wrong

  • Throw exceptions rather then return error codes. When not throwing exceptions, the caller must check for errors immediately after the call (it’s easy to forget to do this) it’s better to throw an exception when you encounter an error
  • Each exception that you throw should provide enough context to determine the source and location of an error. Create informative error messages and pass them along with your exceptions. Mention the operation that failed and the type of failure.
  • Your catch has to leave your program in a consistent state, no matter what happens in the try. So it is good practice to start with a try-catch-finally statement when you are writing code that could throw

Checked Exceptions

Didn’t really know this was a thing since it was something I’d never encountered in the languages I use on a regular bases. But a checked exception is when the signature of every method would list all of the exceptions that it could pass to its caller. Your code literally wouldn’t compile if the signature didn’t match what your code could do. Examples of languages that don’t use checked exceptions: C#, C++, Python, and Ruby. I had to go search what a checked exception looked like, so for those purely swift developers, this is what it looks like in Java:

public void ioOperation(boolean isResourceAvailable) throws IOException {
if (!isResourceAvailable) {
throw new IOException();
}
}

The price of checked exceptions is an Open/Close principle violation. If you throw a checked exception from a method in your code and the catch is three levels above, you must declare that exception in the signature of each method between you and the catch. This means that a change at a low level of the software can force signature changes on many higher levels.

Define Exceptions in Terms of the a Caller’s Needs

How do you classify errors. By their source: did they come from one component or another? By their type: Are they device failures, network failures, or programming errors? The important concern is how they are caught.

They showed an example of poor exception classification, where the snippet of code handled three different exceptions from a third-party library call. The solution to make this better was to create a wrapper class so that it translated the three different exceptions into a common exception type.

Wrapping third-party APIs is best practice. When you wrap a third party API, you minimize your dependencies up it: You can choose to move to a different library in the future without much penalty.

You can see the example in the accompanying github repo, but a snippet of what essentially was done:

class LocalPort {
private let innerPort: ACMEPort
func open() throws {
do {
try innerPort.open()
} catch let error as DeviceResponseError {
throw PortDeviceFailure.portDeviceFailure(error: error)
} catch let error as ATM1212UnlockedError {
throw PortDeviceFailure.portDeviceFailure(error: error)
} catch let error as GMXError {
throw PortDeviceFailure.portDeviceFailure(error: error)
}
}
}

Special Case Pattern

  • You create a class or configure an object so that it handles a special case for you. The client code doesn’t have to deal with exceptional behavior. You can see an example in the accompanying github repo.

Nulls

  • Passing null into methods is worse. you should avoid passing null in your code whenever possible
  • When we return null, we are essentially creating work for ourselves. All it takes is one missing null check to send an application spinning out of control.
// Un-swifty, but matches code in book
func register(item: Item?) {
if item != nil {
let registry: ItemRegistry? = persitentStore.getItemRegistry()
if registry != nil {
let existingItem = registry.getItem(item.getId())
if existingItem.getBillingPeriod().hasRetailOwner()) {
existingItem.register(item)
}
}
}
}
// More Swifty using guard statements.
func register(item: Item?) {
guard let item = item,
let registry = persistentStore.getItemRegistry() else {
return
}
let existingItem = registry.getItem(item.getId())
guard existingItem.getBillingPeriod().hasRetailOwner() else {
return
}
existingItem.register(item)
}

I think returning and passing nil is ok in the iOS world. We have optionals, and if we fail to unwrap an optional we get compile errors. So I think we’re a little safer in that regard in comparison to Java. With that said you still should be smart about when and where you return or pass nil

--

--

Ashley Ng

Mobile Developer @hudl 📱 • @UTCompSci @UTAustin Alumni 🐂