When calling a function which returns data, there is often the need to handle errors which might occur. This post details how to make use of Kotlin’s sealed classes to return one object which can represent the data you expected or an error.

Let’s imagine we have a function which accepts some data as a parameter and returns us the parsed result. It relies on a regex to help with the parsing; given we don’t control the input, that regex might fail to match depending on the string provided to the function.

private fun parse(url: String): ParsedData { val result = URL_PARSE_REGEX.find(url) if (result == null) { // What do we do here? Throw an exception? Return null? } val mimeType = result.groupValues[2] val data = result.groupValues[4] return ParsedData(data, mimeType) } data class ParsedData( val data: String, val mimeType: String )

If the regex fails to match anything, result will be null.

if (result == null) { // What do we do here? Throw an Exception? Return null? }

Note this code 👆. If during the parsing, we decide we can’t continue, we have to find a way to break out of the function.

Potential Solutions (Don’t do these)

Throw an Exception

We might decide one way to achieve this is to throw an Exception .

if (result == null) { throw IllegalArgumentException() }

However, forcing exception handling onto the caller of this function is pretty heavyweight. Even runtime exceptions are still a burden to handle, and not finding a match on a regex doesn’t sound all that exceptional so this probably isn’t the right fit.

Return null

We might decide instead to return null when the regex match fails.

if (result == null) { return null }

Now the caller of this function can perform a null check and use the value being null as an indication that the error flow happened. But we’d also have to define the function as returning ParsedData? now and we have no way of providing details as to what went wrong. All the caller knows is that something didn’t work.

Introduce a wrapper object

We could introduce a new class solely responsible for wrapping both the desired data or an error. Since in this scenario, data and errorMessage are mutually exclusive, we have to make both nullable.

class ResultWrapper ( val data: ParsedData? = null, val errorMessage: String? = null )

We then interrogate the wrapper to determine if an error happened; in this case by checking if result.errorMessage is null or not. Even after determining an error didn’t happen, we still have the annoyance of our real data being nullable by necessity, so we have to force unwrap it using !! .

val result = parse(url) if (result.errorMessage != null) { // error occurred - log error message etc... } else { // happy path - use the data. But result.data is nullable 🙄 val mimeType = result.data!!.mimeType }

Using a wrapper object which might contain the data or an error is better than returning null or throwing an exception, but it is still a little clunky.

Better Solution (Do this)

Sealed Class

Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type.

That definition of a sealed class fits very well with this scenario. We want to be able to return an object from a function which:

Can represent the data we asked for, or

Can represent some sort of error

We don’t want to return just any type of object as we want some compile-time safety around what is returned. As with the basic wrapper approach outlined above, we can change the parse function so that it doesn’t return the ParsedData directly; this time it returns a sealed class .

sealed class ParseResult data class Error(val errorMessage: String) : ParseResult() data class ParsedData( val data: String, val mimeType: String) : ParseResult()

By defining a sealed class we can then start defining other classes which inherit from it. In the example above, we have declared both an Error class and the ParsedData class which holds the data we were trying to obtain. You could even go further and return many different types of error, each of which could have their own fields.

We can modify the parse function now so that it:

Declares ParseResult sealed class as its return type

sealed class as its return type Returns ParsedData when the parsing succeeds

when the parsing succeeds Returns Error when the parsing fails

Our new parse function now looks like this 👇

private fun parse(url: String): ParseResult { val result = URL_PARSE_REGEX.find(url) if (result == null) { return ParseResult.Error("No match found") } val mimeType = result.groupValues[2] val data = result.groupValues[4] return ParseResult.ParsedData(data, mimeType) }

At this point, we haven’t gained much over the basic wrapper approach from before. But when we want to use the ParseResult object, that’s when we see the advantages.

Sealed Classes and when()

Sealed classes pair nicely with the when expression. This is much like Java’s Switch operator except the Kotlin version automatically casts the value for you inside each of the when blocks.

val result = parse(url) when (result) { is ParseResult.ParsedData -> analyzeMimeType(result.mimeType) is ParseResult.Error -> log(result.errorMessage) }

There’s no need to cast to ParsedData before trying to access mimeType , and there’s no need to cast to Error before trying to access errorMessage . 🙏

If you returned the result of the when expression, the compiler would then start highlighting any ParseResult subclasses that you haven’t handled; providing further compile time safety against forgetting to handle new types of the sealed class.

Sealed or Abstract

You could define ParseResult as abstract instead of sealed and you might be wondering why sealed is still better. While abstract would work in this example above, it would also allow something potentially undesirable.

Along with the ParsedData and Error subclasses we’ve defined here, having ParseResult as abstract would also allow subclasses to be defined anywhere in the project.

Whereas with a sealed class for ParseResult you have limited scope in which to define subclasses.

A sealed class can have subclasses, but all of them must be declared in the same file as the sealed class itself.

This helps reduce bugs by ensuring all the different types of possible ParseResult have to be defined in the same class.

Conclusion

When calling a function which returns data, there is often the need to also handle errors which might occur. By using a Kotlin sealed class, you can purposefully restrict the types of data you can return from any given function; limiting the return types to different types of data or of one or more error types.

The different classes returned can each hold completely different data from each other. And yet each distinct type can be handled elegantly when combined with Kotlin’s when expression.

Image Attribution: Photo by Roger Brendhagen on Unsplash