Project Based Learning in Java

Exception Types and Custom Exceptions

Checked and unchecked exceptions, common types, and custom exception design.

Lectures 1.3.3-1.3.4 CO3 aligned Unit 1

Learning outcome: Apply the checked/unchecked compiler rule, recognize common runtime exceptions, and define meaningful custom exceptions.

Java's classification is based on inheritance. The compiler applies the catch-or-declare rule to checked throwable types; it does not apply that rule to unchecked types.

Compiler category Families included
Checked Throwable subclasses outside the RuntimeException and Error families; in application code these are normally subclasses of Exception other than RuntimeException
Unchecked RuntimeException, Error, and all their subclasses

The Error family is also unchecked, but it stands apart from application exceptions because ordinary application code rarely has a useful recovery action for it.

Checked Exceptions

A method that may let a checked exception escape must either catch it or declare it with throws. This is the definition of checked; it does not guarantee that the condition is expected or recoverable.

In simple terms: A checked exception is a registered letter: the compiler will not let you walk past without signing for it, either by catching it or by declaring throws.

Exception Typical cause
IOException File, stream, or network I/O failure
SQLException Database-access failure
ClassNotFoundException A class requested by name cannot be located dynamically
InterruptedException A waiting, sleeping, or joining thread is interrupted
ReflectiveOperationException A reflective operation cannot complete
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CheckedExample {
    static String read(Path path) throws IOException {
        return Files.readString(path);
    }
}

The caller of read must catch IOException or declare it further. Declaring it does not handle it; it transfers responsibility to the caller.

Unchecked Runtime Exceptions

RuntimeException and its subclasses are unchecked. The compiler permits them to propagate without a throws declaration. They commonly report a violated method contract, invalid state, failed conversion, or programming defect, although an input boundary may sometimes recover from them.

Exception Typical cause Prevention or response
NullPointerException Using null where an object is required Establish non-null contracts and validate required inputs
ArrayIndexOutOfBoundsException Index below 0 or at least array.length Check the valid index range
NumberFormatException Text is not valid for the requested numeric type or range Report the invalid input and request another value
ArithmeticException Invalid integer arithmetic, such as division or remainder by zero Validate the divisor
ClassCastException An object is cast to an incompatible reference type Use sound types and check with instanceof when a cast is unavoidable
IllegalArgumentException An argument violates a method contract Validate arguments at the method boundary
IllegalStateException The operation is invalid in the object's current state Preserve and check valid state transitions

Checked vs Unchecked Runtime Exceptions

Question Checked application exception Unchecked runtime exception
Compiler enforces catch or declare? Yes No
Main base type Usually Exception, excluding RuntimeException RuntimeException
May a method still declare it? Yes Yes, but the declaration is optional
Must it be caught immediately? No; it may be declared and propagated No; correct the defect or handle it only at a useful recovery boundary

Checked versus unchecked does not mean recoverable versus unrecoverable. Choose a handler based on whether the current layer can take a useful action.

Focused Examples

NumberFormatException

public class QuantityParser {
    public static void main(String[] args) {
        String input = "12x";

        try {
            int quantity = Integer.parseInt(input);
            System.out.println("Quantity: " + quantity);
        } catch (NumberFormatException exception) {
            System.out.println("Not a valid int: " + input);
        }
    }
}

The text may fail because of invalid characters, unsupported whitespace, or a value outside the int range.

ArrayIndexOutOfBoundsException

int[] marks = {72, 81, 90};
int index = 3;

if (index >= 0 && index < marks.length) {
    System.out.println(marks[index]);
} else {
    System.out.println("Valid indexes are 0 to " + (marks.length - 1));
}

For an expected user-supplied index, validation is clearer than deliberately causing and catching the exception.

IllegalStateException

class Publication {
    private boolean published;

    void publish() {
        if (published) {
            throw new IllegalStateException("Already published");
        }
        published = true;
    }
}

Creating a Custom Checked Exception

Create a named checked exception when a failure represents a meaningful domain condition and callers should be forced to consider handling or propagating it.

In simple terms: Create one when the failure is a real event in your domain — “insufficient balance” rather than “something went wrong”. The class name alone should tell the caller what happened.

public class VoterRegistrationDemo {
    static class InvalidAgeException extends Exception {
        InvalidAgeException(String message) {
            super(message);
        }
    }

    static void register(int age) throws InvalidAgeException {
        if (age < 18) {
            throw new InvalidAgeException("Minimum voting age is 18");
        }
        System.out.println("Registration accepted");
    }

    public static void main(String[] args) {
        try {
            register(16);
        } catch (InvalidAgeException exception) {
            System.out.println(exception.getMessage());
        }
    }
}

Creating a Custom Unchecked Exception

Extend RuntimeException when the condition represents a caller contract violation and mandatory catch-or-declare handling would not improve the API.

In simple terms: Extend RuntimeException when the caller broke the rules rather than met bad luck. A negative age is a programming mistake to fix, not a condition to handle at runtime.

class InvalidPercentageException extends RuntimeException {
    InvalidPercentageException(String message) {
        super(message);
    }
}

class Result {
    private int percentage;

    void setPercentage(int percentage) {
        if (percentage < 0 || percentage > 100) {
            throw new InvalidPercentageException(
                    "Percentage must be from 0 to 100: " + percentage);
        }
        this.percentage = percentage;
    }
}

For this particular rule, the standard IllegalArgumentException would also be a good choice. A custom type is useful only if callers benefit from distinguishing this violation from other invalid arguments.

Custom Exception Design

  1. Give the class a precise name ending in Exception.
  2. Preserve a useful message without exposing sensitive data.
  3. Provide a cause-aware constructor when the exception wraps another failure.
  4. Choose checked or unchecked behaviour deliberately.
  5. Prefer a suitable standard exception when a new domain type adds no value.
class DataImportException extends Exception {
    DataImportException(String message, Throwable cause) {
        super(message, cause);
    }
}

When wrapping, pass the original throwable as the cause so diagnostics retain the full chain.

Common Mistakes

Mistake Correction
Saying only RuntimeException is unchecked Error and its subclasses are unchecked too, though ordinary application code treats them separately.
Assuming checked means recoverable Checked describes compiler enforcement, not outcome or severity.
Catching NullPointerException throughout the program Establish and enforce non-null contracts.
Using checked exceptions for every validation failure Use IllegalArgumentException or another suitable unchecked type for caller contract violations.
Creating a custom type that adds no meaning Prefer an existing standard exception.
Discarding the original throwable while wrapping Pass it as the cause.
Catching Exception and continuing blindly Handle a specific type, add useful context, or propagate the failure.

Practice

  1. Classify IOException, ArithmeticException, ClassNotFoundException, and StackOverflowError by compiler rule.
  2. Validate an array index before access and report the allowed range.
  3. Create a checked InsufficientBalanceException and use it in a withdrawal method.
  4. Add message-only and message-with-cause constructors to a custom exception.
  5. Decide whether a negative method argument should use a standard or custom unchecked exception, and justify the choice.

Quick Check

1. Which throwable families are unchecked?

RuntimeException and Error, including their subclasses, are unchecked. Application discussions often use “unchecked exception” specifically for the RuntimeException branch, so the intended scope should be stated.

2. Must every checked exception be caught in the method where it occurs?

No. The method may declare it with throws and let a caller handle or propagate it.

3. May an unchecked exception appear in a throws clause?

Yes. It may be declared for documentation, but the compiler does not require the declaration.

4. Why preserve an exception's cause?

The cause retains the original diagnostic information and makes the complete failure chain easier to trace.

Summary

The compiler checks throwable types outside the RuntimeException and Error families. Both RuntimeException and Error are unchecked, but ordinary applications handle the two branches differently. Custom exceptions should express a real domain distinction, carry useful context, and preserve their causes.