20 Essential Exception‑Handling Rules to Prevent Crashes in Java

This article presents twenty practical rules for handling exceptions in Java, covering why catching RuntimeException is discouraged, how to avoid using exceptions for control flow, proper use of finally blocks, logging best practices, custom exception design, and early input validation to keep applications stable and maintainable.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
20 Essential Exception‑Handling Rules to Prevent Crashes in Java

Exception handling is a critical part of Java programming; it affects stability, security, user experience, and resource utilization. The article lists twenty best‑practice rules for handling exceptions correctly, using Spring Boot 3.5.0 as the runtime environment.

1. Introduction

The article begins with an overview of common exception types in Java and states that the following sections will detail twenty best practices.

2.1 Avoid catching RuntimeException

Do not catch unchecked exceptions such as NullPointerException or IndexOutOfBoundsException. Prefer pre‑condition checks instead.

Correct example:

if (obj != null) {
  // ...
}
System.out.println(a / b);

Wrong example:

try {
  obj.method();
} catch (Exception e) {
  // swallow
}

If a RuntimeException cannot be avoided (e.g., NumberFormatException), it should still be caught and handled.

2.2 Do not use exceptions for control flow

Exceptions are intended for unexpected error conditions, not regular logic. Using them for flow control incurs high performance cost, reduces readability, and violates design intent.

Performance overhead: throwing and catching is much slower than ordinary condition checks.

Poor readability: logic becomes obscure and hard to maintain.

Design violation: semantics of an exception are lost.

Wrong example:

public class BadSearch {
  public static boolean contains(int[] arr, int target) {
    try {
      for (int i = 0; ; i++) {
        if (arr[i] == target) {
          return true;
        }
      }
    } catch (ArrayIndexOutOfBoundsException e) {
      // use exception to end loop!
      return false;
    }
  }
}

Correct example:

public class GoodSearch {
  public static boolean contains(int[] arr, int target) {
    for (int value : arr) {
      if (value == target) {
        return true;
      }
    }
    return false; // normal termination, no exception
  }
}

2.3 Use finally correctly

Always release resources (files, DB connections) in a finally block, regardless of whether an exception occurs.

public class FinallyBlockExample {
  public static void main(String[] args) {
    FileInputStream file = null;
    try {
      file = new FileInputStream("someFile.txt");
      int data = file.read();
      while (data != -1) {
        System.out.print((char) data);
        data = file.read();
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (file != null) {
        try {
          file.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

Note: Java 7+ offers try‑with‑resources for more concise handling.

2.4 Do not return from finally

Returning inside finally overrides any return from the try block, potentially causing logic errors.

public static int notGood() {
  try {
    System.out.println("execute try block");
    return 1; // this return is ignored
  } finally {
    System.out.println("execute finally block");
    return 2; // final result is 2
  }
}

2.5 Do not ignore exceptions

Even if an exception seems unimportant, it should at least be logged.

public class IgnoreExceptionsBad {
  public void doNotIgnoreExceptions() {
    try {
      int num = Integer.parseInt("pack_xg");
    } catch (NumberFormatException e) {
      // completely ignored, no log
    }
  }
}

Correct handling logs the error and may rethrow:

public class LogExceptionsGood {
  private static final Logger log = Logger.getLogger(LogExceptionsGood.class.getName());
  public void logAnException() {
    try {
      int num = Integer.parseInt("pack_xg");
    } catch (NumberFormatException e) {
      log.error("Failed to parse number: " + e.getMessage());
      throw e;
    }
  }
}

2.6 Catch specific exception subclasses

Catching the most specific exception allows tailored handling.

public class BusinessService {
  private static final Logger LOGGER = Logger.getLogger(BusinessService.class.getName());
  public void processFile() {
    try {
      readFile("nonexistent.txt");
    } catch (FileNotFoundException e) {
      LOGGER.warning("File not found: " + e.getMessage());
    } catch (IOException e) {
      LOGGER.error("File read error: " + e.getMessage());
    }
  }
  private void readFile(String path) throws FileNotFoundException, IOException {
    if (!path.endsWith(".txt")) {
      throw new FileNotFoundException("File not found");
    }
    throw new IOException("Read error");
  }
}

2.7 Provide context in exception logs

Include relevant parameters and exception type in log messages to aid debugging.

public class BusinessService {
  private static final Logger LOGGER = Logger.getLogger(BusinessService.class.getName());
  public void processData(String filePath, int retryCount) {
    try {
      readData(filePath);
    } catch (IOException | SQLException e) {
      LOGGER.error("Business processing failed [filePath: {}, retryCount: {}, errorType: {}]",
                   filePath, retryCount, e.getClass().getSimpleName(), e);
    }
  }
  private void readData(String path) throws IOException, SQLException {
    if (path == null) {
      throw new IOException("File path cannot be null");
    }
    throw new SQLException("DB connection timeout");
  }
}

2.8 Use custom exceptions when built‑in ones are insufficient

public class MyException extends Exception {
  public MyException(String message) {
    super(message);
  }
}

public void doSomething() throws MyException {
  throw new MyException("Specific error message");
}

2.9 Preserve stack trace when rethrowing

When wrapping an exception, pass the original as the cause.

catch (NoSuchMethodException e) {
  // wrong: loses original stack trace
  throw new MyServiceException("Info: " + e.getMessage());
}

// correct
catch (NoSuchMethodException e) {
  throw new MyServiceException("Business execution error", e);
}

2.10 Prefer standard exceptions

public void setValue(int value) {
  if (value < 0) {
    throw new IllegalArgumentException("Invalid argument");
  }
  // ...
}

2.11 Avoid printStackTrace() in production

Printing stack traces can leak sensitive data, degrade performance, and produce incomplete information in multi‑threaded environments. Use a logging framework (log4j, slf4j, logback) instead.

try {
  // business code
} catch (Exception e) {
  logger.error("An error occurred", e); // logs stack trace safely
}

2.12 Do not catch Throwable

Catching Throwable also captures serious errors (e.g., OutOfMemoryError) that should not be handled.

public void doNotCatchThrowable() {
  try {
    // risky operation
  } catch (Throwable t) {
    // avoid this
  }
}

2.13 Use try‑with‑resources

Java 7+ can automatically close resources, eliminating manual finally cleanup.

try (FileInputStream input = new FileInputStream("file.txt")) {
  int data = input.read();
  while (data != -1) {
    System.out.print((char) data);
    data = input.read();
  }
} catch (IOException e) {
  // log with framework
}

Multiple resources can be managed together:

try (FileInputStream input = new FileInputStream("input.txt");
     FileOutputStream output = new FileOutputStream("output.txt")) {
  byte[] buffer = new byte[1024];
  int length;
  while ((length = input.read(buffer)) > 0) {
    output.write(buffer, 0, length);
  }
} catch (IOException e) {
  e.printStackTrace();
}

2.14 Provide detailed context when throwing

public void loadConfiguration(String path) throws IOException {
  try {
    // load logic
  } catch (IOException e) {
    throw new IOException("Failed to load configuration file, path: " + path, e);
  }
}

2.15 Do not log and then rethrow the same exception

Logging and rethrowing creates redundant output and can confuse error tracing.

// wrong
try {
  // risky code
} catch (NumberFormatException e) {
  log.error(e);
  throw e;
}

// correct – wrap with business exception
public void wrapException(String input) throws MyBusinessException {
  try {
    // risky code
  } catch (NumberFormatException e) {
    throw new MyBusinessException("Invalid input: " + input, e);
  }
}

2.16 Do not throw from finally

Throwing in finally masks the original exception.

try {
  someMethod(); // may throw exceptionOne
} finally {
  try {
    cleanUp(); // may throw exceptionTwo
  } catch (Exception e) {
    log.error("Cleanup failed", e); // log only, do not rethrow
  }
}

2.17 Throw exceptions relevant to the method

Expose meaningful exceptions to callers instead of low‑level ones.

public static int divide(int a, int b) throws ArithmeticException {
  if (b == 0) {
    throw new ArithmeticException("Division by zero");
  }
  return a / b;
}

2.18 Validate user input early

Validate inputs before performing operations to catch errors early.

Connection conn = ...;
try {
  conn.setAutoCommit(false);
  validateUserInput();
  insertUserData(conn);
  validateAddressInput();
  insertAddressData(conn);
  conn.commit();
} catch (SQLException e) {
  if (conn != null) {
    try { conn.rollback(); } catch (SQLException ex) { System.err.println("Error: " + ex.getMessage()); }
  }
  System.err.println("Error: " + e.getMessage());
} finally {
  if (conn != null) {
    try { conn.close(); } catch (SQLException e) { System.err.println("Error: " + e.getMessage()); }
  }
}

2.19 Log an exception only once

In multithreaded environments, separate log statements can interleave; combine related messages into a single log entry.

// wrong
log.error("Error1");
log.error("Error2");

// correct
log.error("Error1, Error2…");

2.20 Use try‑finally when you do not intend to handle the exception

If you only need cleanup and want the exception to propagate, omit the catch block.

try {
  method1(); // may call method2 which throws
} finally {
  cleanUp(); // always executed
}

Conclusion

Effective Java exception handling follows these principles: avoid catching unchecked exceptions, never use exceptions for flow control, release resources in finally or via try‑with‑resources, log with a proper framework, preserve original stack traces, prefer standard or specific exceptions, provide rich contextual information, and validate inputs early. Following the twenty rules helps keep applications robust, readable, and maintainable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaException HandlingBest PracticesLoggingSpring Boottry-with-resources
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.