Pick a markdown and code style:

Error Handling with Monads

Chuck.C.Liang@Hofstra.edu

Computer scientists are human and as such they make mistakes. Famous computer scientist Anthony Hoare made numerous important contributions including Quicksort and Monitors (as in semaphores and monitors). Yet he also admitted to making one horrible mistake. At a software conference in 2009, he apologized for inventing the null pointer. In his own words,

"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years."

In the spirit of admitting mistakes, I confess that during most of the 25+ years that I've been teaching students how to program, I made little effort to properly teach error handling techniques. I've written many programs for teaching my classes but few of them handled errors properly. Take for example the following function (in Java, but it could be in many languages), which returned the largest value in an array of integers:

  public static int largest(int[] A) {
    int answer = A[0];
    for(int i=1;i<A.length;i++)
      if (A[i]>answer) answer = A[i];
    return answer;
  }

This function fails to handle multiple errors, one of them is when the actual argument A is null. It's also possible to have a non-null but zero-length array, in which case A[0] would throw and ArrayIndexOutofBounds exception.

My excuses for writing such erroneous code are numerous:

  1. Most academic computer scientists are not engineers and are concerned with more intellectually meaningful problems. Some academics would just say that the domain of the function is "restricted to non-empty arrays", as if that would make the problem go away.

  2. It's already so hard to just teach students how to program and appreciate algorithmic principles such as time/space complexity. Adding error-handling code distracts from the main principles and makes teaching that much harder.

  3. The error-handling paradigm employed by Java and many other languages, namely the raising and catching of exceptions, is not sufficient.

I was certainly not alone in making the first two excuses. The first I now find embarrassing; the second has some truth to it, but is still not justifiable. Only the third excuse is something I will still claim as valid today. More and more people are coming to realize that exceptions are not a good approach to error-handling.

In the event of an error when calling largest, Java will throw either a NullPointerException or ArrayIndexOutOfBoundsException.
In a semester when I'm "good", I would add a line to the above function

  if (A==null || A.length==0)
    throw new RuntimeException("invalid input to function 'largest'");

Of course throwing an exception alone is not enough: they have to be caught. But where should the exception be caught? First of all, there is often no way to handle the exceptions locally. In this example, we cannot effectively add a try-catch block inside the function because it's still obliged to return an integer. But in the event of an empty array, any integer that it returns would surely be wrong. We have no choice but to differ error handling elsewhere. This is unfortunate because we want our function to encapsulate an algorithm. However, the error handling aspect of the algorithm escapes the encapsulation.

There is also no clear, general strategy as to where to catch the exceptions externally. If we simply place a try-catch around every call to the function, we may well run into the same problem again (what to return from the calling function). There is another problem with exceptions, which is that they're dynamically scoped. The exception is caught by the nearest catch as determined at runtime. In other words, if we write:

  try {
    if (/*some condition*/) f(); else g();
  } catch (NullPointerException npe) {  /* error handling code */  }

It is not possible to say with certainty that "any null pointer exception that occurs inside the try block will be handled by the provided error handling code." It may be the case that function f or g has it's own exception handler. It depends on what happens at runtime. This dynamic nature of exceptions makes it even harder for us to write code that encapsulate error handling. Even if all exceptions are required to be eventually caught (which subclasses of RuntimeException are not), the problem of exception handling is a global, and not a local one.

And in a semester when I'm "bad", I'd just write

  public static void main(String[] args) throws Exception

just to avoid having to catch any darn exception.

Whether it's Java or another language, the fact is that exceptions run contrary to the principles of abstraction and information hiding that we'd like to have in high-level programming. Of course it is possible to write error-free programs by catching and handling all exceptions in just the right places, but there are few general strategies on how to do so.

Even in situations where error-handling code with exceptions are not difficult to write, the whole process of error-handling can still be tedious, repetitive and distracting from the main task of implementing an algorithm. As an academic computer scientist trained originally in functional and logic programming, I'd much to prefer to write "declarative" code. We call it "referential transparency". Suppose we wish to apply a series of functions to a value, we'd like to just write "h(g(f(x)))" or maybe "x.f().g().h()". But we often can't, because f, for example, may return null or some invalid value not acceptable to g. We have to break up the simple, declarative code and add a bunch of if-else checks and try-catch blocks. Now the code looks ... decidedly not declarative.

Monads

I'd like to tell my students "whenever you find yourself writing code that's tedious and repetitive, it's time to find a higher form of abstraction that will encapsulate the tediousness and allow us to write code that's simple and clear again." Using a variable instead of many constants, using a vector instead of many variables, writing a function that encapsulates an algorithm, writing a generic class are all examples of what I mean by this kind of abstraction.

A monad is an abstraction that applies to virtually all functions and types. Monads in programming originated from the study of category theory and its application to functional programming languages (e.g. Haskell and OCaml). Most programming languages including C++ and Java are acquiring more functional programming features.

Some advanced stuff ...

Mathematically, a monad is a monoid in the category of endofunctors. In the following definitions I will use both functional programming syntax in the form of lambda expressions, as well as object-oriented syntax in the form of method calls on objects. A lambda expression is a nameless function that implements (in Java) an interface containing one required function. For example, (x -> x*1.5) would implement java.util.function.Function<Integer,Double>. The type of the lambda expression is inferred from the interface that it instantiates.

A monad is defined by specifying a Kleisli Triple:

  1. A map from types to types. In programming language syntax, a template/generic class or interface M that maps any type A to M<A>.
  2. A function unit from values of type A to values of type M<A>
  3. A function bind of type M<A> -> (A -> M<B>) -> M<B>. That is, a function that takes an object of type M<A> and then a function from A to M<B>, and returns an object of type M<B>.

I will write applications of the bind combinator in the object-oriented style: m.bind(f) to mean that bind is applied to m, then to the function f, where f is usually a lambda expression. This will make the syntax of composing a series of binds much cleaner: m.bind(f).bind(g).bind(h) ....

Such a triple must form a (non-commutative) monoid. The unit map must be a left-identity of bind in the sense that x -> (unit(x).bind(f)) (unit composed with bind) is equivalent to f. It should also be possible to see unit as a right-identity in the sense that a.bind(unit) is equivalent to a. In full-scale functional programming languages such as Haskell, bind is assigned an infix operator >>= so we can write these laws more naturally as unit(x) >>= f = f(x) and a >>= unit = a. Furthermore, bind must be associative in the sense that

(a >>= f) >>= g = a >>= (x -> (f(x) >>= g))

In java-ish syntax, this means

a.bind(f).bind(g) is equivalent to a.bind(x -> f(x).bind(g)).

Associativity is an important component of referential transparency.

For example, unit(3).bind(x -> unit(x*0.5)) may return an object of type M<Float> that's equivalent to unit(1.5). In a particular monad, the bind operation may perform some additional work before applying its functional argument to the encapsulated value, such as checking if the value is null. In this way bind can encapsulate "mundane" tasks such as error handling.

This may be a lot to take in for someone who's not familiar with functional programming, and certainly can be very confusing to intro-level students. There are various tutorials that try to explain monads in simplified terms. One such tutorial even claims that if you understood what monads are, you will lose the ability to explain it to others. I believe that the best way to teach the monad abstraction is to start with a very specific monad, which also happens to be how monads are used most often in programming: the encapsulation of error-handling.

A monad maps programs from an imperfect world, fraught with errors, to a more perfect world of mathematical functions that can be composed without obstruction.

import java.util.Optional;

Java is by now almost three decades old and so much of that language is based on exception handling and the possibility of pointers being null. null is the default value of any reference type. Perhaps no other popular language is more susceptible to the null pointer problem than Java. It will be difficult to completely avoid null, and exceptions, but modern java is beginning to offer some alternatives. It is be possible to define our own monadic type within java (and we will do so below) but Java offers the java.util.Optional type which is intended to be used with the interfaces of java.util.function.

Optional is the first component of the Kleisli triple: a type constructor that maps any type T to type Optional<T>. The second element, unit, comes in the form of the Optional.of function, whereas bind is called Optional.flatMap. In addition to these, there are a number of other functions on the Optional class, such as map and orElse. Not all of these methods are mathematically pure. The get method is definitely not. As the name suggests, an object of type Optional<A> possibly holds a value of type A. It is also possible that the object does not contain a value. An "empty Optional" is formed with the method Optional.empty(). (Please note that the requirements of a Kleisli triple says nothing about the contents of objects, only their types, so .empty is not considered part of the triple but is specific to the Optional monad).

We can rewrite the "largest" function as follows:

public static Optional<Integer> largest(int[] A) {
    if (A==null || A.length==0) return Optional.empty();
    int answer = A[0];
    for(int i=1;i<A.length;i++)
      if (A[i]>answer) answer = A[i];
    return Optional.of(answer);
  }

Notice that the error handling code is local, independent of what happens elsewhere. While the original largest function is not a total function on all values of type int[], the new function is. The function does not return an integer. If one tried to use the return value as an integer, it will result in a compiler error. Compiler errors are virtually zero-cost compared to runtime errors.

  int[] A = {4,3,7,2};
  int z = 3 * largest(new int[]{4,3,7,2}); // compiler error!

The programmer is forced to confront the fact that it's possible that no value was returned by the function call. When using the value returned by such a function, we must apply flatMap (bind) or a similar function such as map. map is a specialized form of flatMap that takes an Optional<A>, a function A -> B and returns an Optional<B> object. a.map(f) is equivalent to a.flatMap(x -> Optional.of(f(x))).

  largest(new int[]{4,3,7,2})
    .map(x -> 3 * x)
    .ifPresent(x -> {System.out.println("Result = "+x);});

The key here is that .map will have no effect on the monad if it is empty. It will not throw any exceptions; it would just return an empty optional. If the array passed to largest is null or of zero length, the code just does nothing. This is because the bind (flatMap) of this monad checks if the value is actually present before apply its function to it. The .ifPresent method is only different from map in that the mapped procedure does not return a value.

On the other hand, we can also write

  var max = 
    Optional.of(new int[]{})
      .flatMap(x->largest(x)) // java won't accept .flatMap(largest)
      .orElseGet( () -> {  //retrieves value or apply procedure to compute it
           System.err.println("no largest value found, returning 0 as default");
           return 0;
       });

Which would set max to 0 in settings where such a default is appropriate.

As another sample application of Optional, we create an object adapter for java.util.HashMap. The principle methods of this class are put(key,value) and get(key). The get(key) function of HashMap returns null if no value associated with the key is found. This fact alone must be responsible for some of that billion dollars worth of pain that Hoare initiated. Our object adapter will try to alleviate the suffering.

import java.util.Optional;
import java.util.HashMap;
public class SafeHash<KT,VT> {
  private HashMap<KT,VT> HM; // internal object to be adapted to new interface
  public SafeHash() { HM = new HashMap<KT,VT>(); } // constructor
  public int size() { return HM.size(); }
  
  public Optional<VT> get(KT key) {    
     VT val = H.get(key); // can be null
     if (val==null) return Optional.empty();
     else return Optional.of(val);
  }//get
  
  public SafeHash<KT,VT> put(KT key, VT val) {
    if (key!=null && val!=null) HM.put(key,val);
    return this;
  }//SafeHash returns 'this' so operations can be chained using .
}

We can apply the new class as follows:

  var GPV = new SafeHash<String,Double>(); // grade-point value map
  GPV.put("A", 4.0)
     .put("B", 3.0)
     .put("C", 2.0)
     .put("D", 1.0)
     .put("F", 0.0)
     .get("E")   // non-existent key, get empty Optional
     .map(x -> x * 2.0) // still empty Optional, but no error
     .ifPresent(x -> System.out.println("it's worth twice as much at Hofstra: "+x)).

Nothing will be printed by this code, but there is no null pointer exception, which is what you would get with java.util.HashMap.get. Of course, you can also avoid the exception by checking if (... == null), but the point is that there is no requirement to write the if. The monadic approach, on the other hand, leads you to write code that would not compile if the possibility of errors are not taken into consideration.

Imperfections

As mentioned above, it's difficult to be rid of exceptions and null pointers completely in a language like Java. Even with the Optional class there are ways to circumvent the safety that monads promise. Chief among these is the Optional.get method. Given a value x of type Optional<A>, x.get() will return a value of type A. But if there is no value to get, then an exception will be throw, and we're back to where we started. An irresponsible programmer can call .get() on any Optional-typed object without checking if the Optional actually contains a value. The justification for .get() may be that some programmers are just not comfortable with lambda expressions, and will not be able to write any programs without it. But the potential cost is that it could render the concept useless, and Optional.empty() will suffer much of the same consequences as the null pointer. We can at least experiment with trying to live without .get.

A User Defined Monad Type

It is, however, possible to define our own Monad. In the following three classes I define for Java the rough equivalent of the Result Monad found in Rust and other languages. Yes, since embracing Rust much of my perspective on programming have changed. But the issue of error handling and monads predates Rust and is relevant to all programming languages. Result<A,B> is a disjunctive type: it consists of either a value of type A or a value of type B. Usually, A represent successful computations and B represent error objects, replacing exception objects. In our examples, B is String, indicating some error message. The monadic functors bind, map, etc take functions that instantiate java.util.function interfaces designed for functional programming. In particular, Function<A,B> represent functions A -> B. Typically, functions are allowed to be contravariant in the domain (? super A) and covariant in the codmain (? extends B).

Result<V,E> is implemented as a public abstract superclass with two non-public subclasses, OkResult holding a value of type V and ErrResult holding a value of type E. The constructors of the subclasses are internal to the package. Publicly, Result objects can only be created by calling the functions Ok and Err.

Besides the required monadic functions (unit is Ok), I've added several more that might be useful, including match and match_do. Each of these functors take two functions, to be applied to either an Ok value or an error (Err) object. Not all methods are mathematically sound, such as map_mutate, which changes an existing Result instead of creating a new one. It's added for efficiency. Missing however, is the .get function of the Optional class. The closest methods to .get are orElse, which requires a default value to be specified in case there is nothing to get, and getOr, which requires a function to be specified that computes a compatibly typed value from an error object. Our implementation of this monad is purer then even Rust's, which contains a .get()-like function (unwrap).

package ResultMonad;
import java.util.function.*;

public abstract class Result<V,E> {   
    // Abstract methods
    public abstract boolean is_ok();  // contains V value if true
    public abstract V orElse(V Default);
    public abstract V getOr(Function<? super E,? extends V> errfun);
    public abstract <U> Result<U,E> map(Function<? super V, ? extends U> f);
    public abstract <U> Result<U,E> bind(Function<? super V, Result<U,E>> f);
    public abstract void ifOk(Consumer<? super V> c);
    public abstract <U> U match(Function<? super V, ? extends U> okfun,
                Function<? super E, ? extends U> errfun);
    public abstract void match_do(Consumer<? super V> okfun,
                  Consumer<? super E> errfun);
    public abstract Result<V,E> map_mutate(Function<? super V,? extends V> mut);
    
    // Non-abstract methods
    public <U> Result<U,E> flatMap(Function<? super V, Result<U,E>> f) {
	return bind(f);
    }  // flatMap is alias for bind
    
    public static <A,B> Result<A,B> Ok(A v) {
	return new OkResult<A,B>(v);
    }//static, polymorphic Ok
    public static <A,B> Result<A,B> Err(B e) {
	return new ErrResult<A,B>(e);
    }//static, polymorphic Err

    // apply a function W*V -> U on two results
    @SuppressWarnings("unchecked")
    public <U,W> Result<U,E> chain(Result<W,E> other, BiFunction<? super V, ? super W, ? extends U> binop) {
      return this.match(
	_ok1 -> other.map(_ok2 -> binop.apply(_ok1,_ok2)),
	_err -> (ErrResult<U,E>)this  //new ErrResult<U,E>(_err)
      );
    }//chain

    // if other result is Err, do not change current result
    @SuppressWarnings("unchecked")    
    public <W> Result<V,E> if_and(Result<W,E> other, BiFunction<? super V, ? super W, ? extends V> binop) {
	return other.match(
			   _ok -> this.map(_ok1 -> binop.apply(_ok1,_ok)),
			   _err -> this
			   );
    }//if_and
    
    // combine two results into one for a pair
    public <U> Result<pair<V,U>,E> zip(Result<U,E> second) {
        return this.bind(x -> second.bind(y -> Ok(new pair<V,U>(x,y))));
    }//zip (non-abstract) function

    // transform into java.util.Optional
    public java.util.Optional<V> toOptional() {
        return 
            this.match(_ok -> java.util.Optional.of(_ok),  //ok
                       err -> java.util.Optional.empty() //err
                       );
    }//toOptional
    
}//Result abstract class
class OkResult<V,E> extends Result<V,E>
{
    private V val;
    OkResult(V v) { // constructor accessible only inside package
        if (v==null) throw new Error("null pointers not allowed in result");
        val=v;
    }
    public V orElse(V Default) { return val; }
    public V getOr(Function<? super E,? extends V> errfun) {
        return val;
    }
    public boolean is_ok() {return true;}
    
    public <U> Result<U,E> map(Function<? super V, ? extends U> f) {
        return new OkResult<U,E>(f.apply(val));
    }//map
    
    public <U> Result<U,E> bind(Function<? super V, Result<U,E>> f) {
        return f.apply(val);
    } //bind (flatmap)

    public void ifOk(Consumer<? super V> c) { c.accept(val); }

    public <U> U match(Function<? super V, ? extends U> okfun,
		       Function<? super E, ? extends U> errfun) {
        return okfun.apply(val);
    }//match

    public void match_do(Consumer<? super V> okfun,
                         Consumer<? super E> errfun) {
	okfun.accept(val);
    }//match_do

    public Result<V,E> map_mutate(Function<? super V,? extends V> mut) {
	val = mut.apply(val);
	return this;
    }

}//OkResult
@SuppressWarnings("unchecked")
class ErrResult<V,E> extends Result<V,E>
{
    private E errobj;
    ErrResult(E e) { errobj = e; }
    public boolean is_ok() {return false;}
    public V orElse(V Default) { return Default; }
    public V getOr(Function<? super E,? extends V> errfun) {
        return errfun.apply(errobj);
    }    
    public <U> Result<U,E> map(Function<? super V, ? extends U> f) {
	return (ErrResult<U,E>)this; //get compiler warning if not suppressed
        //return new ErrResult<U,E>(errobj); //no warning but not efficient
    }//map
    public <U> Result<U,E> bind(Function<? super V, Result<U,E>> f) {
	return (ErrResult<U,E>)this;	
    } //bind (flatmap)
    public void ifOk(Consumer<? super V> c) {}
    public <U> U match(Function<? super V, ? extends U> okfun,
		       Function<? super E, ? extends U> errfun) {
        return errfun.apply(errobj);
    }//match
    public void match_do(Consumer<? super V> okfun,
                         Consumer<? super E> errfun) {
        errfun.accept(errobj);
    }//match_do

    public Result<V,E> map_mutate(Function<? super V,? extends V> mut) {
	return this;
    }

}//ErrResult

Unlike an empty Optional object, an ErrResult object carries more information, usually in the form of some error message. The following complete program demonstrates the ResultMonad package. I've included two monadic functions, safemod, which will not divide by zero, and an adapted version of java's Integer.parseInt function, which is often used to process command-line arguments.

import ResultMonad.*;

public class resulttesting
{
    // for convenience
    static <A,B> Result<A,B> Ok(A x) { return Result.Ok(x); }
    static <A,B> Result<A,B> Err(B x) { return Result.Err(x); }

    static Result<Integer,String> safemod(int x, int y) {
	if (y==0) return Err("division of "+x+" by zero");
	else return Ok(x%y);
    }

    // adopt existing Java method to use Result instead of exceptions
    static Result<Integer,String> parseint(String[] argv, int i) {
	Result<Integer,String> answer = Err("invalid args to parseint");
	if (argv==null || i<0 || i>=argv.length) return answer;
	try {
	    answer = Ok(Integer.parseInt(argv[i]));
	}
	catch (Exception e) {
	    answer = Err("string "+argv[i]+" cannot be parsed to integer");
	}
	return answer;
    }//parseint
    
    public static void main(String[] args) {
	parseint(args,0)   // Ok or Err
	    .map(x -> x*x) // Ok if above is Ok, else Err
	    .map_mutate(x -> x-9) // Ok if above is Ok, else Err
	    //.bind(x -> safemod(100,x))  // Ok or Err
	    .match(
		 _ok -> safemod(100,_ok),
		 _err -> Err(_err+";\ncan't continue due to previous error")
	     )
	    .bind(x -> parseint(args,1).map(y -> (Integer)x-y))
	    .chain(parseint(args,1),
		   (x,y) -> x+y
	     )
	    .match_do(
		 _ok -> {
		     System.out.println("Result = "+ _ok);
		 }, // Ok case
		 _err -> {
		     System.out.println("ERROR: "+ _err);
		 }  // Err case
	     ); // match_do
    }//main
}//resulttesting
// run java resulttesting
//     java resulttesting 10 20
//     java resulttesting 3
//     java resulttesting abc

// The typecast (Integer)x is required because of possible contravariance
// in the domain of the function passed to bind.

Monads in Other Conventional Languages

As demonstrated, the monad can be implemented in any object oriented programming language. Newer languages restrict where null is allowed to appear. Kotlin's compiler even enforces that an object's been checked to be null before allowing it to be accessed. C++ 2017 added an optional type, but the monadic functions that are equivalent to bind, map, etc. are not available until C++ 2023 is released. However, I've written an experimental option_ptr class in C++ that will try to combine the functionality of a monad with that of a smart pointer. The implementation will not rely on virtual functions like in Java in difference to the core C++ concept of Zero Overhead Abstraction. Instead, the internal representation will use nullptr, but it will be presented to the user as an empty optional object.

Continuation

There cannot be a "conclusion" to these thoughts because the final word on the effectiveness of the monadic approach to error-handling is yet to be written. We cannot answer the question academically: it would have to come from the cumulative experiences of the software industry in the coming decades. It is a fact that the trend in programming language development is turning against exception-handling and becoming more functionally oriented. The success of new languages such as Rust, which embraces the approach we espoused here, gives us hope that we're on the right path.

From a pedagogical perspective, yes there's a lot for programmers to learn in order to apply this new approach to handling errors. But the learning curve would not be as steep once a series of good examples becomes available. These examples will form a new set of design patterns for the composition of error-free programs.

In the Spring semester of 2023, I for the first time have abandoned exceptions in teaching an intermediate level course on data structures and object oriented programming in Java. It is always difficult to teach higher forms of abstraction. Many students struggle with understanding and appreciating the simplest form of functional abstraction. It gets progressively more difficult as we introduce recursive functions, second-order functions, interfaces and abstract classes, etc. Monads are yet another level of abstraction. Yes, it makes teaching even more difficult, but what choice do we have? We cannot continue to preach to our students a pretty picture of abstract data types and encapsulated algorithms when error handling doesn't fit the picture. Monads allow us to return to the pulpit. The effort that we will spend teaching, and the effort that students will spend on learning these advanced programming techniques are still far less costly than another billion dollars worth of pain.

  1. ResultMonad Java package folder
  2. resulttesting.java testing program
  3. option_ptr.cpp experimental monad/smart pointer in C++