5

Given a language that enforces checked exceptions (let us assume Java), how do we design an API that can throw exceptions without violating the Law of Demeter?

To put this question in the proper scope, let us assume we are writing a publicly accessible jar library. Some of the classes will be the public-facing API: others are internal bits and pieces not meant to be directly used outside the library.


For example, perhaps this library has the responsibility of serializing objects through a stream or writer: the interfaces for OutputStream and Writer specify that output operations may throw the checked exception IOException. The specific stream/writer implementation is irrelevant: IO exceptions may be thrown.

Let us further assume that the write operations do not occur in response to a method call that one would reasonably assume results in an IO operation, e.g. saveTo(File) or sendTo(URL).

If the library cannot reasonably recover from the exception, it must pass it down the call stack to the appropriate level where it may be handled. However, this leaks information about distant parts of the system, violating the Law of Demeter.


It appears as though there are three ways to deal with this situation.

  1. Declare the method and all methods that call it as throwing the checked exception, all the way to the API boundary. The client code must then deal with the checked exception whether it wants to or not. However, the exception is at least documented.

    This is a bit brittle: implementation details deep in the library dictate the public interface of other classes. This is pretty much what the Law of Demeter tells us to avoid.

  2. Wrap checked exceptions in an unchecked exception:

    try { ... }
    catch (SQLException e) {
      throw new UncheckedException(e);
    }
    catch (IOException e) {
      throw new UncheckedException(e);
    }
    

    This fixes the interface dilemma, but puts another burden on client code: it must know to look for unchecked exceptions, which in this case may not be your typical logic bug (e.g. null pointer, divide by zero) but an exception case that can be reasonably handled by code. However, it is now no longer part of the public interface, and if it is, it is optional. Furthermore, all checked exceptions are now in a single unchecked class, making it difficult to catch one but not another.

  3. Wrap each checked exception which may be thrown in a dedicated unchecked exception:

    try { ... }
    catch (SQLException e) {
      throw new UncheckedSqlException(e);
    }
    catch (IOException e) {
      throw new UncheckedIoException(e);
    }
    

    This fixes the interface problem, allows each exception type to have its own unchecked exception type, but results in an explosion in the number of exception classes required.


How can we design a public API in such a way that checked exceptions can be thrown without violating the Law of Demeter, where classes far away (the client code) know too much about the inner workings of the library that publishes said public API, while still being able to deal with error conditions that arise in the library?

See Also

  • 3
    Don't use checked exceptions. – Robert Harvey Mar 7 '16 at 23:05
  • 1
    @RobertHarvey That is not always possible. I pick on Java because its authors decided to inflict checked exceptions upon us in Java's Standard Library. – user22815 Mar 7 '16 at 23:08
  • Perhaps the answer is not about how to avoid breaking it, but knowing when to break it. – Zymus Mar 8 '16 at 0:36
  • @RobertHarvey What exactly is your point against checked exceptions? Are they bad? Could you perhaps describe why? I have always used checked exceptions. – Andy Mar 8 '16 at 7:16
7

The Law of Demeter is sometimes paraphrased as “counting dots” in an expression like object.field.anotherField.someMethod().fieldOfResult. The problem is that the API surface of one object doesn't just consist of the operations and fields of that object, but also of the API surface of all exposed fields. While the API surface is usually defined in terms of method parameters and return types, the set of exceptions that can be thrown directly or indirectly also contributes to the API surface.

By default, methods are transparent to the (unchecked) set of exceptions that can be thrown. If a method a() calls method b() which throws an exception, that exception is passed through the call stack until some handler can be found. Since b() might be user-provided rather than known at compile time, we cannot ascribe a fixed API surface to a(). While we can construct various cases were this dynamic is necessary, it is often preferable to fix the possible exceptions statically (hence static typing is a thing).

To be clear, all your three supposed solutions keep on violating the LoD. By wrapping the caught exceptions in unchecked exceptions you are merely disguising them, but they still contribute to the effective API surface of your method. As I see it we have two available solutions:

  • If the method is just a thin wrapper over a specific library function and is tightly coupled anyway, then forwarding the checked exceptions in a throws declaration is probably the right thing to do. The LoD is a heuristic rather than a law, and it is frequently sensible to ignore it.

  • If the checked exception is part of integral base libraries of your program, you are already so tightly coupled that you may just forward them in most cases. This assumes the used code is not an implementation detail, but a central feature of your code. This applies to standard libraries of the language and other 3rd partly libraries you're using in a similar manner.

  • In most cases, think about the user. When someone uses your code, what was their intention? What went wrong from their perspective? What do they need to know? How can they react to the exception? If any such exception is “game over”, converting it to some runtime exception is the way to go.

    But if the exception can be reasonably handled, the possibility of this exception must be known, it must have an appropriate type so that it can be caught selectively, and it must communicate how the user's intention failed. That the problem was caused by an SQLException or IOException is not irrelevant information, but it should not contribute to your API surface. Instead, we would define our own checked exception type to wrap the cause. A domain-appropriate exception type might be DataSourceNotAvailableException or MessageSendFailedException or TextureLoadingException. Compare the following signatures:

    send(Message m) throws NotAuthenticatedException, SQLException, IOException {
      authenticateWithApi();
      logToDatabase(m);
      lowLevelSend(m);
    }
    
    send(Message m) throws NotAuthenticatedException, LoggingException, IOException {…}
    
    send(Message m) throws NotAuthenticatedException, MessageSendFailedException {
      authenticateWithApi();
      try {
        logToDatabase(m);
        lowLevelSend(m);
      } catch (LoggingException|IOException e) {
        throw new MessageSendFailedException(e);
      }
    }
    

    As a guideline, each operation should have a single responsibility, and therefore only a single non-forwarded checked failure mode. By consistently throwing only our own checked exceptions in our code, we'll end up with many more exception classes, but the problem of a “combinatorial explosion” of throws annotations is limited.

  • Good analysis. And +1 for "The LoD is a heuristic rather than a law, and it is frequently sensible to ignore it." – user949300 Mar 8 '16 at 15:41
4

Checked exceptions are not bad. Checked exceptions warn developers that exceptional circumstances must be properly handled or their program will crash or misbehave. In an ideal world things would be 100% deterministic, but the real world isn't like that. Disks fill up, networks go down, etc.

If you use a SQL DB under the hood and you can guarantee with certainty it will never fail or you don't mind that when it does your program or users of your library crash then go ahead and swallow the exception or rethrow it as a runtime exception.

There are choices about what checked exceptions you expose on your public API methods, you need not declare methods throw SQLException, you can choose to either wrap it in a checked exception of your own creation, perhaps PersistenceException, or you can declare your method to throw one of the parent classes such as IOException. After all you might change your API to use something other than SQL under the hood some day. This would be a good way to follow the Law of Demeter as close as reasonably possible. Many of these so-called software architecture laws and principles should be more appropriately called guidelines.

You can't and shouldn't hide the fact that your library needs network connectivity or a working file system any more than a car manufacturer should hide the gas cap. The best you can do is minimize the surface area it covers and make sure it is clearly marked.

  • This reads more like an analysis of checked vs. unchecked and less of an analysis of its impact on the Law of Demeter. – user22815 Mar 8 '16 at 18:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy