Just My Type! — Custom Types and Error Handling in Swift

Tommy Bajis
4 min readOct 10, 2017

--

The First Error

Before you sit down to code, it’s important to address the first error you’ll need to handle, not try-ing.

Reading documentation or taking the time to learn about error handling isn’t exactly most people’s idea of a fun time. It definitely wasn’t for me, at first. However, after exploring some tutorials and contemporary literature on the subject, I’ve found a certain appreciation in constructing custom types to deal with errors.

Functional Swift — A Fantastic Resource

Whether you’re a tenured Swift Developer, or someone just getting started, chances are that you’ve heard about Functional Programming.

Functional Swift is a fantastic resource and introduction into Functional Programming in Swift. While this resource highlights many great topics, I found the sections on Enumerations and Swift Errors to be particularly useful in my projects. This post will take you through some examples inspired from those sections.

The examples below are available in a playground in my Demos Repository. Feel free to clone or download the repo and follow along!

The Problem

I’m sure that we’ve all written methods to check for certain values in arrays or dictionaries. These container types are a part of the Swift Standard Library and are integral to iOS Development. In the examples that follow, we’ll refine a method that retrieves the number of team members using only an individual’s memberName. However, getting the number of team members requires looking up the team first. A reasonable approach to constructing this method might look like this:

Using an Optional Return Type

func numberOfTeamMembers(memberName: String) -> Int? {   guard let team = teamNames[memberName] else { return nil }
guard let number = teamMembers[team] else { return nil }
return number
}

We could then call this method in our code like so:

let someLookup = numberOfTeamMembers(memberName: "Tommy")

Not bad, right? Our numberOfTeamMembers(memberName:) method will return an optional Int? if the lookup is successful. Otherwise, in the event that one of the checks fail, someLookup will contain nil. While this may seem like a reasonable approach to solving our problem, we know nothing about why a lookup could fail. In other words, should the method above return nil, we don’t have any information as to which of the checks did not pass. For more complex methods, this could pose a problem when tracking down the source of a lookup error.

A Solution: Enter Enumerations

One of the shortcomings of the numberOfTeamMembers(memberName:) method lies in its return type Int?. What if we could create our own return type? Specifically, a type that could return an Int if the lookup is successful and return a detailed error if it fails. To do so, we could try something like this:

enum LookupError: Error {
case teamMemberNotFound
case numberOfMembersNotFound
}
enum LookupResult {
case success(Int)
case failure(LookupError)
}

Here we’ve created a custom return type, LookupResult, that contains two enumeration cases, success(Int) and failure(LookupError). A success case will be associated with an Int and a failure will be associated with a LookupError. Note that LookupError contains two enumeration cases, teamMemberNotFound and numberOfMembersNotFound. These cases just happen to represent the two checks that could fail in our previous method! Now we could re-write our method like so:

func numberOfTeamMembers2(memberName: String) -> LookupResult {
guard let team = teamNames[memberName] else {
return .failure(.teamMemberNotFound)
}
guard let number = teamMembers[team] else {
return .failure(.numberOfMembersNotFound)
}
return .success(number)
}

Similarly, we could call this method like this:

let newLookup = numberOfTeamMembers2(memberName: "Dan")

Now newLookup will contain an Int in the case of a successful lookup and a LookupError in the case of failure. While we had to apply a little more overhead to get this result, it demonstrates how beneficial custom types can be when needing to handle errors in our projects. Although, if we wanted to, we could take this example one step further.

One Step Further: Enter Generics

While the previous example did help solve our problem, it could be improved upon. Ideally, we’d like our LookupResult type to be more generalized so that it can be used for different tasks. Basically, we’d like to not have to create a new result type for each method or problem that we solve in our projects.

To apply Generics to our previous example, we could create a new result type and a new generalized look up method like so:

enum NewResult<T, SomeError: Error> {
case success(T)
case failure(SomeError)
}
func newLookup(memberName: String) -> NewResult<Int, LookupError> {
guard let team = teamNames[memberName] else {
return .failure(.teamMemberNotFound)
}
guard let number = teamMembers[team] else {
return .failure(.numberOfMembersNotFound)
}
}

The NewResult<T, SomeError: Error> type can be used to return any generic type T on success and any SomeError on failure (so long as SomeError conforms to the Error protocol). The precision of this generic return type allows us to take advantage of Swift’s type checking and can prevent bugs before your project has been tested. We could even switch on the result of the method we wrote above for clarity.

let teamMembersResult = newLookup(memberName: "Samantha")switch teamMembersResult {
case .success(let number):
// TODO: handle success flow here
case .failure(let error):
// TODO: handle error flow here
}

In Conclusion

The purpose of using the result type above is not to completely replace the implementation of optionals in our projects. After all, we did have to define custom enumerations to solve what was a relatively simple problem. In addition, interfaces that consistently favor custom types over existing types will be less familiar to the occasional contributor.

However, using the NewResult<T, SomeError: Error> type is a great way to handle errors in Swift. Using enumeration cases helped us define types that were specific to the problem.

--

--

Tommy Bajis

Software Engineer | iOS Developer | University of Michigan Grad | Baseball fan | Spider-Man and Iron Man enthusiast