Return Codes versus Exceptions
The C-Style Error-Handling Pattern
In languages without exceptions (C, early C++, Go), functions signal errors by returning a sentinel value or setting a global error variable:
int fd = open("data.txt", O_RDONLY);
if (fd == -1) {
fprintf(stderr, "Cannot open file: %s\n", strerror(errno));
return 1;
}
ssize_t bytes = read(fd, buffer, sizeof(buffer));
if (bytes == -1) {
fprintf(stderr, "Read error: %s\n", strerror(errno));
close(fd);
return 1;
}
for (ssize_t i = 0; i < bytes; i++) {
if (process(buffer[i]) != 0) {
fprintf(stderr, "Processing failed\n");
close(fd);
return 1;
}
}
close(fd);
The two lines of actual logic (open, read, process, close) are buried in a tangle of error checks, resource cleanup, and early returns. The normal flow and the error flow are interleaved, making the code hard to read and reason about.
Three Problems with Return Codes
1. Callers Can Silently Ignore Errors
Nothing forces the caller to check the return value:
open("data.txt", O_RDONLY); // return value discarded — error goes undetected
The compiler offers no help. The program silently continues with an invalid file descriptor, potentially corrupting data or crashing later at a point far from the root cause.
2. Normal Logic Is Obscured
The error-handling code and the normal-case logic are interleaved. A reader must mentally subtract the error-checking boilerplate to see what the function actually does. In the C example above, the function’s purpose — “open a file, read it, process each byte” — is lost in the noise of if (x == -1) checks and close(fd) calls.
3. Valid Returns Can Be Confused with Error Sentinels
If a function returns -1 for both “error” and “valid negative result”, the calling code cannot distinguish them. In C, fgetc() returns int rather than char precisely because it needs the sentinel for EOF/error — the return type expands to accommodate an error value that is otherwise meaningless.
The Exception Model
Java separates error detection from error handling:
try {
FileInputStream fis = new FileInputStream("data.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
int b;
while ((b = bis.read()) != -1) {
process((byte) b);
}
} catch (IOException e) {
System.err.println("I/O error: " + e.getMessage());
}
The normal-path code is clean — it describes what the program does in the success case. The error-handling code is segregated in the catch block, where it does not obscure the main logic. Multiple operations that might fail can share a single catch block, rather than requiring an individual check after each statement.
Crucially, for checked exceptions, the compiler forces acknowledgement — the caller must either handle the exception or declare that it throws the exception upward. You cannot silently ignore the possibility of failure.
Comparison
| Aspect | Return Codes | Exceptions |
|---|---|---|
| Separation of concerns | Error handling mixed with normal logic | Error handling in separate catch blocks |
| Can caller ignore? | Yes — compiler does not enforce checking | Checked exceptions: no (compiler enforces). Unchecked: yes |
| Propagation | Caller must manually propagate errors up the stack | Exceptions propagate automatically through any number of callers |
| Performance (success case) | Fast (no stack trace) | Fast (zero cost on success path in modern JVMs) |
| Performance (failure case) | Fast | Relatively expensive — stack trace capture |
| Type safety | Error values may conflict with valid return values | Exceptions are objects with their own type hierarchy |
When Exceptions Are the Wrong Tool
Exceptions should not be used for ordinary control flow. Throwing and catching an exception is relatively expensive because the JVM must capture a stack trace (walking the call stack, creating StackTraceElement objects). Using exceptions to exit a loop or signal a “not found” condition that is actually a normal, expected outcome is both semantically misleading and slow:
try {
while (true) {
process(iterator.next());
}
} catch (NoSuchElementException e) {
// using an exception to detect end-of-iteration — bad practice
}
Instead, use the standard iteration pattern:
while (iterator.hasNext()) {
process(iterator.next());
}
Exceptions are for exceptional conditions — things that should not normally happen and from which the immediate caller is not expected to recover.