Exception Handling

In Java code, exception handling crops up for three reasons:

  • Wrapping checked exceptions (see Checked Exceptions if you’re unfamiliar with checked exceptions)

  • Using a finally block to clean up nonmemory resources, such as file and network handles

  • Responding to the problem: ignoring the exception, retrying the operation, converting the exception to a nonexceptional result, and so on

In Clojure, things are similar but simpler. The try and throw special forms give you all the capabilities of Java’s try, catch, finally, and throw. But you shouldn’t have to use them very often, because Clojure doesn’t require you to deal with checked exceptions, and there are helpful macros like with-open to encapsulate resource cleanup.

Let’s see what this looks like in practice.

Keeping Exception Handling Simple

Java programs often wrap checked exceptions at abstraction boundaries. A good example is Apache Ant, which tends to wrap low-level exceptions (such as I/O exceptions) with an Ant-level build exception:

 try​ {
  newManifest = ​new​ Manifest(r);
 } ​catch​ (IOException e) {
 throw​ ​new​ BuildException(...);
 }

In Clojure, you’re not forced to deal with checked exceptions. You don’t have to catch them or declare that you throw them. So the previous code would translate to the following:

 (Manifest. r)

The absence of exception wrappers makes idiomatic Clojure code easier to read, write, and maintain than idiomatic Java. That said, nothing prevents you from explicitly catching, wrapping, and rethrowing exceptions in Clojure. It simply is not required. You should catch exceptions when you plan to respond to them in a meaningful way, and in the next exception, we’ll see how Clojure handles this in its data-centric way.

Rethrowing with ex-info

In Java it’s common to create many custom exception subclasses corresponding to all manner of contingencies. Often these are built in deeply nested exception hierarchies. In practice, the majority of these exception classes add little value beyond their specific class name, which can be caught and handled.

In Clojure, we instead have a single custom exception class provided with the language (IExceptionInfo), which carries a map of data where you can place any information that’s necessary or useful to handle the error.

For example, a common use for custom exceptions is inside the code that serves a web request. Various error conditions might result in different HTTP status codes. Rather than have dozens of exceptions, we can simply throw a custom exception with a map of data, including the status code that should be returned:

 (​defn​ load-resource
  [path]
  (try
  (​if​ (forbidden? path)
  (throw (ex-info ​"Forbidden resource"
  {:status 403, :resource path}))
  (slurp path))
  (catch FileNotFoundException e
  (throw (ex-info ​"Missing resource"
  {:status 404, :resource path})))
  (catch IOException e
  (throw (ex-info ​"Server error"
  {:status 500, :resource path})))))

The load-resource function first checks to see whether the resource is forbidden. If so, we throw a 403. Otherwise, we try to read and return the resource. We also handle two different types of errors—the case where something is missing and an unknown IO failure. In all of these cases, we use the ex-info function to create a custom exception instance with a message and the map of data.

Higher up the call stack, some other code can catch this exception (with type IExceptionInfo) and retrieve the map of data using ex-data. This handler or middleware could then construct the proper HTTP response message to return.

When using external resources, it’s important to properly close and dispose of those resources. Often in Java this can become a tangle of exception handling code. In the next section, we’ll look at some options Clojure provides for easier cleanup.

Cleaning Up Resources

Garbage collection will clean up resources in memory. If you use resources that live outside of garbage-collected memory, such as file handles, you need to make sure that you clean them up, even in the event of an exception. In Java, this is normally handled in a finally block.

If the resource you need to free follows the convention of having a close method, you can use Clojure’s with-open macro:

 (with-open [name init-form] & body)

Internally, with-open creates a try block, sets name to the result of init-form, and then runs the forms in body. Most important, with-open always closes the object bound to name in a finally block. A good example of with-open is the spit function in clojure.string:

 (clojure.core/spit file content)

spit simply writes a string to file. Try it:

 (spit ​"hello.out"​ ​"hello, world"​)
 -> nil

You should now find a file at hello.out with the contents hello, world.

The implementation of spit is simple:

 ; from clojure.core
 (​defn​ spit
 "Opposite of slurp. Opens f with writer, writes content, then
  closes f. Options passed to clojure.java.io/writer."
  {:added ​"1.2"​}
  [f content & options]
  (with-open [^java.io.Writer w (apply jio/writer f options)]
  (.write w (str content))))

spit creates a PrintWriter on f, which can be just about anything that is writable: a file, a URL, a URI, or any of Java’s various writers or output streams. It then prints content to the writer. Finally, with-open guarantees that the writer is closed at the end of spit.

If you need to do something other than close in a finally block, the Clojure try form looks like this:

 (try expr* catch-clause* finally-clause?)
 ; catch-clause -> (catch classname name expr*)
 ; finally-clause -> (finally expr*)

You can use it as follows:

 (try
  (throw (Exception. ​"something failed"​))
 (finally
  (println ​"we get to clean up"​)))
 | we get to clean up
 -> java.lang.Exception​:​ something failed

The previous fragment also demonstrates Clojure’s throw form, which simply throws whatever exception is passed to it.

Responding to an Exception

The most interesting case is when an exception handler attempts to respond to the problem in a catch block. As a simple example, write a function to test whether a particular class is available at runtime:

 ; not caller-friendly
 (​defn​ class-available? [class-name]
  (Class/forName class-name))

This approach is not very caller-friendly. The caller just wants a yes/no answer but instead gets an exception:

 (class-available? ​"borg.util.Assimilate"​)
 -> java.lang.ClassNotFoundException​:​ borg.util.Assimilate

A friendlier approach uses a catch block to return false:

 (​defn​ class-available? [class-name]
  (try
  (Class/forName class-name) true
  (catch ClassNotFoundException _ false)))

The caller experience is much better now:

 (class-available? ​"borg.util.Assimilate"​)
 -> false
 
 (class-available? ​"java.lang.String"​)
 -> true

Clojure gives you everything you need to throw and catch exceptions and to cleanly release resources. At the same time, Clojure keeps exceptions in their place. They’re important but not so important that your mainline code is dominated by the exceptional.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.239.234