Do or Do Not: Patterns and Antipatterns with the Try type
By: Brian Mearns
Tags: Software, Java, Scala, Functional Programming, Software Development

The Try type is a really useful mechanism for handling the possibility of errors in Java, Groovy, Scala, and other languages. It originates (as far as I can tell) in Scala, a functional programming language where it fits comfortably among other functors and monads (don’t worry, it doesn’t matter); it’s been incorporated into imperative languages like Java, but the procedural baggage of those languages contain hidden dragons for its use.

In this article, we’ll go over a brief explanation of the Try type then explore some antipatterns that are commonly encountered with it and wrap up with some more appropriate patterns for its use.

What is Try?

First things first: let’s distinguish the Try type — which is the subject of this article — from the try statement. Both deal with exceptions, but the former is a type (e.g., an interface or a class) introduced into the language by a library, while the latter is a keyword integrated into the language’s grammar. Values of type Try can be passed around like any other and are of no special significance to the runtime, whereas try indicates a statement, which has its unique flow control baked into the runtime.

A Try is just an object that represents the results of an operation that may have failed. It contains exactly one of two values: a successful value, or an exception indicating the failure. A minimalist interface might look something like this:

The core conceptual methods of the Try type

The Try type typically has two concrete implementations: Try.Success and Try.Failure. The code below illustrates the general concept of the type (though not best practices):

Sample code (not recommended use) illustrates the basic conceptual model

Try, in other words

If you’re familiar with any of these other concepts, they might help draw parallels to Try:

A Try is kind of like an Either where one side is the successful value, and the other is an exception.

A Try is somewhat like an Optional, but instead of present or not, it’s successful or not and, if not, it records the error.

A Try is a bit like a synchronous observable stream, with either a single event or an error.

A Try is basically like a settled Promise, it has either succeeded (fulfilled) or failed (rejected).

More to Try

If the two-method interface shown above was all that Try had to offer, its use would be pretty straight forward, and rather limited. And there’d be no need for this article. So let’s take a quick look at some of the other common methods on Try.

A method you’re likely to use often is map:

When invoked on a Success, it applies the given transform to the wrapped value and returns a new Success wrapping the result of the transform. When invoked on a Failure type, the Failure simply returns itself.

The map function allows you to conditionally operate on the result of some function only when that function was successful; if the function failed, the operation isn’t applied and you’re still left with the original failure.

The flatMap method is similar but handles the case where the transform function itself returns a Try; this is useful when the operation you want to conditionally perform could itself fail. Its signature looks like this:

The following shows a simple example of how map and flatMap might be used (still not entirely best practices, but we’ll get there):

Illustrative example of Try::map and Try::flatMap

The map and flatMap methods are useful for handling successes; for failures, the recover and recoverWith methods are helpful.

The recover method is used to transform a Failure into a Success by applying a function that maps an exception into a success value. The recoverWith method is also used to handle failure cases, but its mapper function returns a Try; the relationship between recover and recoverWith is analogous to the difference between map and flatMap.

Manifestations of Try often provide a variety of other methods, but they’re all more-or-less variations on the same theme: you have a Try which encapsulates something — either a value representing success, or an error — and you apply functions to transform that something into something else; only that something else is still encapsulated inside a Try (making Try a functor, for what it’s worth).

These transformations are the key to effective use of Try: you transform your encapsulated value (or error) however many times it takes to get what you want and then use get to either unpack the encapsulated value or throw the encapsulated exception.

Importantly, if any of the transformations fail, those failures get encapsulated as well and returned as a Failure. This means you don’t have to handle each possible failure on its own, they all get rolled up into the chain of Try values.

Antipatterns

Most antipatterns that are seen with Try come from not appreciating the functional nature of the interface, treating it more as a container than as an encapsulation.

Like all antipatterns, these are not absolute; seeing these in code should raise an eyebrow and stir some investigation, but that doesn’t mean there’s never a good reason to use them. The key is to use them intentionally, not out of ignorance.

With that in mind, let’s take a look at some of the most common ways that Try is misused.

Unpacking Try Values Too Early

The general usage pattern for Try is to encapsulate the result of an operation that may have failed in a Try, transform the result using methods like map or recover, and finally unpack the Try using get.

The problem is when you omit the middle of that pattern: the transformation. The transformation methods are what make the Try type so powerful because they already know how to handle the difference between a Success and a Failure. When you unpack a Try too early you lose out on this power and you end up with a lot of repetitive code to handle everything that Try could have handled for you. This includes wrapping transformations that could potentially fail inside of a try block, recovering errors from inside a catch block and then getting the results out of that block, and terminating the chain of transformations anywhere you encounter a non-recoverable error.

Returning Try from a Method

A lot of misuse of Try comes as a result of having a Try when you really shouldn’t. A common cause of this is when a Try is returned by a method. If you want to represent a possible failure in the method, throw an exception, that’s exactly what they’re for; using a Try for this purpose is unnecessary.

Using a Try implies that you have specific transformations that you want to apply to the result. But a method isn’t in charge of what happens to its result, that’s up to the calling code. If the calling code wants to encapsulate the result of calling the method in a Try that’s easy enough for it to do; doing so in the method adds an unnecessary layer of complexity.

You can think of this as a corollary to the principle that you shouldn’t catch an exception if you don’t know what to do with it: likewise, you shouldn’t encapsulate a result in a Try if you don’t have something to do with it.

Using new Try

We saw in a sample of code above how you can create a new instance of either the Try.Success subtype of the Try.Failure subtype. This code was meant to illustrate the basic concepts of the type and its concrete implementations, but doing this isn’t usually recommended.

Instead, the Try.apply static factory is invoked with a function and returns a Try encapsulating the result: a Success with the returned value or a Failure with any error that was thrown.

Using the apply function removes a bit of boilerplate, like the try/catch block, which would otherwise distract from the real intent of your code. It also lets you mark the Try variable as constant, which typically makes for cleaner code.

More to the point, constructing a Try directly is often an indicator that you’re using it when you don’t need to be. As suggested above, a lot of times when you could use a Try, you could just as easily use the try/catch functionality that your language already provides. You’ll need that try/catch eventually to unpack the Try and avoiding unnecessary use of the Try type can avoid additional layers of indirection and obfuscation that make your code harder to read.

When To Try

With all of these reasons not to use Try, is there ever a good case for using it?

Absolutely.

The best time to use a Try is when you need to perform an operation that could fail, but you can’t throw an exception. This comes up a lot when using higher-order functions: functions that return functions or that take functions as arguments.

There are two specific cases when you might not be able to throw an exception. The first is when you need to use a type that doesn’t allow for checked exceptions, like most of the interfaces in the java.util.function package (e.g., Function, Consumer, and Supplier).

The second is when you’re performing multiple independent operations and you don’t want a failure in one to interfere with the others. By using Try, you can perform all the operations, capture any failures, and process them all at the end.

Conclusion

The Try type was introduced into imperative JVM languages by way of the functional Scala language; it brings with it the potential to chain fallible transformations, honor non-throwing interfaces, and easily defer handling of failures without interrupting successes. It also brings with it the potential for abuse through misuse and overuse, much of which is the result of misunderstanding its purpose, which is primarily to deal with exceptions when you can’t throw them.

You should consider adding Try to your toolbox, but only after you understand both how and when to use it.