Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

Java Exception Handling: The Good, The Bad and The Ugly

From Wiki
Jump to: navigation, search
Article Metadata
Article
Created: grahamhughes (15 Jan 2010)
Last edited: lpvalente (02 Feb 2014)

Contents

Introduction

When writing a method, you frequently have to indicate back to the caller that something has gone wrong. There are two ways to do this:

  1. Returning some "fail" value (like null, false, or -1)
  2. Throwing an exception

Overview

I don't approve of using return values to indicate success or failure. In C, yes, this is fine, because C has no structured exception handling model. But not in languages that have exceptions... exceptions exist to provide failure information (or success information, by their absence).

Examples

Why do I dislike "fail values"? Because of what you will see in C programs. Let's say you have methods like:

/**
* @returns object acquired, null if acquisition fails
*/

public AcquiredObject acquireObject(ObjectType t);
 
/**
* @returns true if processing successful, false if processing fails
*/

public boolean processObjects(AcquiredObject o1, AcquiredObject o2, AcquiredObject o3);

These will get used in one of three ways.

// no exceptions - example 1: when structured programming goes bad
public int useObjects() {
int errorCode;
AcquiredObject ao1, ao2, ao3;
if ( (ao1 = AcquiredObject.acquire(FIRST)) != null ) {
if ( (ao2 = AcquiredObject.acquire(SECOND)) != null ) {
if ( (ao3 = AcquiredObject.acquire(THIRD)) != null ) {
if (!processObjects(ao1, ao2, ao3)) {
errorCode = 4;
} else {
errorCode = 0;
}
ao3.release();
} else {
errorCode = 3;
}
ao2.release();
} else {
errorCode = 2;
}
ao1.release();
} else {
errorCode = 1;
}
return errorCode;
}

Structured programming is a good thing, lots of research shows that it reduces developments costs and timescales, increases readability, increases code quality, reduces maintenance costs, etc. This code will work well, but look at it: all the structure is involved in handling failures, and it's hard to find where the actual work is being done.

// no exceptions - example 2: too many returns
public int useObjects() {
AcquiredObject ao1, ao2, ao3;
if ( (ao1 = AcquiredObject.acquire(FIRST)) == null ) {
return 1;
}
if ( (ao2 = AcquiredObject.acquire(SECOND)) == null ) {
return 2;
}
if ( (ao3 = AcquiredObject.acquire(THIRD)) == null ) {
return 3;
}
 
if (!processObjects(ao1, ao2, ao3)) {
return 4;
}
 
ao3.release();
ao2.release();
ao1.release();
return 0;
}

This looks nice and clean, more readable. But, if anything fails, the release() calls get missed, resulting in resource-leaks.

// no exceptions - example 3: ignored return values
public void useObjects() {
AcquiredObject ao1 = AcquiredObject.acquire(FIRST);
AcquiredObject ao2 = AcquiredObject.acquire(SECOND);
AcquiredObject ao3 = AcquiredObject.acquire(THIRD);
 
processObjects(ao1, ao2, ao3);
 
ao3.release();
ao2.release();
ao1.release();
}

Looks cleaner still, but now no attempt is made to handle problems. The code will just blindly attempt to continue working no matter how futile. The program will probably end up throwing difficult-to-trace NullPointerExceptions, that don't describe anything about what actually went wrong.

Let's change the method specifications to use exceptions.

public AcquiredObject acquireObject(ObjectType t) throws ObjectAcquisitionException;
public void processObjects(AcquiredObject o1, AcquiredObject o2, AcquiredObject o3) throws ProcessObjectException;

Where ObjectAcquisitionException and ProcessObjectException both extend Exception (not RuntimeException), and so must be handled.

How will programmers use these methods?

// exceptions - example 1: scared of exceptions
public void useObjects() {
AcquiredObject ao1 = null;
AcquiredObject ao2 = null;
AcquiredObject ao3 = null;
try {
ao1 = AcquiredObject.acquire(FIRST);
} catch (Exception e) {}
try {
ao2 = AcquiredObject.acquire(SECOND);
} catch (Exception e) {}
try {
ao3 = AcquiredObject.acquire(THIRD);
} catch (Exception e) {}
 
try {
processObjects(ao1, ao2, ao3);
} catch (Exception e) {}
 
ao3.release();
ao2.release();
ao1.release();
}

This programmer just sees exceptions (and the requirement to handle them) as an annoyance, and has added code to swallow them and allow the code to carry on. This reproduces "no exceptions - example 3", ignoring any information about errors that might help you debug the code.

Programmers will often "justify" the catch{} blocks, by filling them with something useless like "e.printStackTrace();". Useless, because most devices don't have any console to print this too.

// exceptions - example 2: trying to return fail values
public int useObjects() {
AcquiredObject ao1, ao2, ao3;
try {
ao1 = AcquiredObject.acquire(FIRST);
} catch (ObjectAcquisitionException e) {
return 1;
}
try {
ao2 = AcquiredObject.acquire(SECOND);
} catch (ObjectAcquisitionException e) {
return 2;
}
try {
ao3 = AcquiredObject.acquire(THIRD);
} catch (ObjectAcquisitionException e) {
return 3;
}
 
try {
processObjects(ao1, ao2, ao3);
} catch (ProcessObjectException e) {
return 4;
}
 
ao3.release();
ao2.release();
ao1.release();
return 0;
}

This is written by a programmer who is used to using return values to report status, rather than exceptions. Again, resources won't get released properly. Also, any information in the exception object about what went wrong is lost, making debugging harder.

// exceptions - example 3: trying to release safely
public boolean useObjects() {
AcquiredObject ao1 = null;
AcquiredObject ao2 = null;
AcquiredObject ao3 = null;
boolean success = false;
 
try {
ao1 = AcquiredObject.acquire(FIRST);
} catch (ObjectAcquisitionException e) {}
try {
ao2 = AcquiredObject.acquire(SECOND);
} catch (ObjectAcquisitionException e) {}
try {
ao3 = AcquiredObject.acquire(THIRD);
} catch (ObjectAcquisitionException e) {}
 
if (ao1 != null && ao2 != null && ao3 != null) {
try {
processObjects(ao1, ao2, ao3);
success = true;
} catch (ProcessObjectException e) {}
}
 
if (ao3 != null) {
ao3.release();
}
if (ao2 != null) {
ao2.release();
}
if (ao1 != null) {
ao1.release();
}
return success;
}

This programmer is making an attempt to return some success/error status, and also to make sure that objects get released properly. But it's a lot of code.

Also, notice that these programmers are having to initialize variables to null, to avoid the compiler complaining that variables might not have been initialized at the point where they're being used. This compiler feature is useful: it checks your code to make sure that you've actually got a useful value in each variable. If you just pre-initialize variables to some useless value (like null), you basically switch this check off, so it can't help you anymore.

Initializing variables to null also has a tendency to produce confusing NullPointerExceptions, especially when they end up getting used as return values. NullPointerExceptions are one of the least useful exceptions, because:

  1. They can be thrown from almost any line of code, making it hard to guess from where they are being thrown. They can be hard to find even with a stack trace (because CLDC stack traces don't have line numbers). On device, a stack trace is usually a luxury you won't have.
  2. They are RuntimeExceptions, and so unchecked. Java does not force you to catch them.
// exceptions example 4: using exceptions
public void useObjects() throws ObjectAcquisitionException, ProcessObjectException {
AcquiredObject ao1 = AcquiredObject.acquire(FIRST);
try {
AcquiredObject ao2 = AcquiredObject.acquire(SECOND);
try {
AcquiredObject ao3 = AcquiredObject.acquire(THIRD);
try {
processObjects(ao1, ao2, ao3);
} finally {
ao3.release();
}
} finally {
ao2.release();
}
} finally {
ao1.release();
}
}

This time, I don't need to initialize everything to null, I don't need to check the value, and I don't keep catching exceptions all over the place. Everything will get released correctly, no matter what goes wrong. If something goes wrong, the caller of this method will receive a precise exception object, which can describe the failure in detail (through its type and its message attribute), much more useful than a simple integer or boolean, or a NullPointerException. On the emulator (or phones with on-device debug), I can get a stack trace that will pinpoint the problem. The caller can't ignore the exceptions like it can ignore a return value: Java will force the exception to be thrown or caught, increasing the chance that it will get dealt with. Even though the nesting is fairly deep, the code is still quite short and simple. The work is not all wrapped up in ifs, or in many tiny try..catch blocks.

Conclusion

Java enforces two useful rules:

  1. Exceptions (other than RuntimeExeptions) must be caught or declared as thrown
  2. Local variables must be explicitly assigned a value before being used


These are there to help you. If I ever find them an inconvenience, I see it as a warning that I need to think about restructuring my code.

See also: Exception Handling in Java

This page was last modified on 2 February 2014, at 11:58.
163 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×