Skip to content
Part IA Michaelmas Term

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 1-1 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

AspectReturn CodesExceptions
Separation of concernsError handling mixed with normal logicError handling in separate catch blocks
Can caller ignore?Yes — compiler does not enforce checkingChecked exceptions: no (compiler enforces). Unchecked: yes
PropagationCaller must manually propagate errors up the stackExceptions 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)FastRelatively expensive — stack trace capture
Type safetyError values may conflict with valid return valuesExceptions 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.