Parameter-Dependent Exceptions

Suppose you have a method in a class used to save some data in the database. The method has a parameter telling whether to perform business validation prior to saving. When that option is enabled the implementation will throw an exception in case the data is invalid, and when the option is not set it will save whatever it manages to save, never throwing an exception. The former case would be used when the data is published, and the latter case could be used for automatic draft saving for instance (drawing scenario inspiration from WordPress here).

We could simply duplicate the methods:

public void save(Data data);

public void saveAndValidate(Data data) throws InvalidDataException;

But for this post we’ll assume we don’t want to do that (maybe there are tons of methods of this sort, or maybe there’s a long stack of service layers that delegate to each other and we don’t want to do this method duplication everywhere).

I’ll analyse robustness against two future evolutions:

  • Invocation-side: a developer copy-pastes an invocation of the method to somewhere else but enables validation in the new location.
  • Implementation-side: a developer decides that in some case we should really throw a validation exception even when validation is disabled.

Level Seven

“Exceptions are not well suited for this case because when validation is disabled we don’t want to force the call-site to handle an exception that can’t happen. Let’s use return codes instead”

/** @return true if the call was successful. */
public boolean save(Data data, boolean validate);

call site:

/* Save draft */
save(draft, false);

or

/* Publish */
if (!save(data, true)) {
    /* Handle validation error here. */
}

Now a developer comes along, sees the first invocation, copies somewhere else, replaces false by true. If there’s an error in the data it simply won’t be saved, without any warning or error whatsoever. → Level Seven.

If another developer decides that even save(draft, false) should sometimes throw exceptions for really bad problems, the first code snippet above will simply fail silently. → Level Seven.

Level Six

What if we used unchecked exceptions?

/** The throws statement is only for documentation
 * purposes, as the exception derives RuntimeException. */
public void save(Data data, boolean validate) throws UncheckedInvalidDataException;

call site:

/* Save draft */
save(draft, false);

or

/* Publish */
try {
    save(data, true);
} catch (UncheckedInvalidDataException e) {
    /* Handle validation error here. */
}

Now if the first code is copied somewhere, the false being changed to true, we get an error at runtime, elevating this design to Level Six. Similarly if the method one day throws the exception even when validate is false, we get an error at runtime.

Adding unit tests that feed bad data to the save(draft, false); call elevates the design to Level Four for implementation-side evolutions (call-site evolutions stays at Level Six because your unit tests likely won’t be invoking the other location to which the developer copied the data).

Level Three

Using checked exceptions really elevate the design to Level Three for call-site evolution:

public void save(Data data, boolean validate) throws InvalidDataException;

call site:

/* Save draft */
try {
    save(draft, /*validate*/false);
} catch (InvalidDataException e) {
    throw new RuntimeException(e); // can't happen, we don't validate
}

or

/* Publish */
try {
    save(data, true);
} catch (InvalidDataException e) {
    /* Handle validation error here. */
}

Call-site evolution produces this:

try {
    save(data, /*validate*/true);
} catch (InvalidDataException e) {
    throw new RuntimeException(e); // can't happen, we don't validate
}

This is obviously wrong! Line two says /*validate*/true which obviously contradicts the comment two lines below.

Note that the /*validate*/ comment before the false parameter is crucial. If you omit it, it is no longer obvious that the true enables the exception without going to the save() declaration, and the requirements for Level Two clearly state “obvious by looking at a single code section in a single file“.

If you omit the rethrow with a RuntimeException you fall back to Level Seven with respect to the implementation-side evolution!

Level One

There is a way to produce variable throws exceptions: putting a type parameter in a throws statement. For instance this interface uses such a variable throws statement:

public interface ThrowingCallback<E extends Exception> {
    public void run() throws E;
}

The following interface replaces boolean in the save method to take advantage of this feature:

public interface ValidationToggle<E extends Exception> {
    void validateIfNeeded(ThrowingCallback<InvalidDataException> validator) throws E;

    public static final ValidationToggle<RuntimeException> NO_VALIDATION =
        new ValidationToggle<RuntimeException>() {
            public void validateIfNeeded(ThrowingCallback<InvalidDataException> validator) {}
        };

    public static final ValidationToggle<InvalidDataException> DO_VALIDATE =
        new ValidationToggle<InvalidDataException>() {
            public void validateIfNeeded(ThrowingCallback<InvalidDataException> validator)
            throws InvalidDataException {
                validator.run();
            }
        };
}

The save() method:

public <V> void save(final Data data, ValidationToggle<V> validator) throws V {
    /* Insert any preparatory work here */
    validator.validateIfNeeded(new ThrowingCallback<InvalidDataException>() {
        public void run() throws InvalidDataException {
            /* validate 'data' here throwing InvalidDataException if something is bad in it */
        }
    });
    /* save 'data'. */
}

Call-site:

/* save draft */
save(draft, ValidationToggle.NO_VALIDATION);

or

/* publish */
try {
    save(data, ValidationToggle.DO_VALIDATE);
} catch (InvalidDataException e) {
    /* Handle validation error here */
}

This design is Level One with respect to both evolutions. If a developer copies the “save draft” snippet and replaces NO_VALIDATION by DO_VALIDATE, compilation will fail because checked exception InvalidDataException (which is inferred by the compiler for parameter V) is not handled. → Level One.

If another developer decides to throw an InvalidDataException outside of the validateIfNeeded block (to make sure it is executed even when validate == NO_VALIDATION), the compilation will fail because InvalidDataException is not declared in the throws statement of the save method (only E is). → Level One.