Java is great until something goes wrong, because one unexpected error, and your whole program crashes. But what if you could stop that from happening?
That’s where try
and catch
comes in. They let you handle errors before they derail your program, keeping everything running smoothly.
In this guide, you’ll learn how to catch errors the right way, so debugging feels less like a guessing game and more like problem-solving.
Let’s dive in.
Sidenote: If you find that you’re struggling with the topics in this guide, or perhaps feel that you could use some more training, or simply want to build some more impressive projects for your portfolio, then check out my Java programming bootcamp:
Updated for 2025, you'll learn Java programming fundamentals all the way from complete beginner to advanced skills.
Better still, you’ll be able to reinforce your learning with over 80 exercises and 18 quizzes. This is the only course you need to go from complete programming beginner to being able to get hired as a Backend Developer!
With that out of the way, let’s get into this 5-minute tutorial…
No matter how well Java developers write their code, errors will happen. Some come from coding mistakes, like dividing by zero or forgetting to initialize a variable. Others are out of the developer’s control, such as missing files, failed internet connections, or bad user input.
Java categorizes these errors into two types:
NullPointerException
)Since checked exceptions are unavoidable, Java requires developers to handle them themselves, because if they aren’t caught, the program will crash.
That’s exactly what try
and catch
are for. We can assume errors will happen and engineer solutions in advance. So if X problem happens, do Y instead of crashing.
Or in Java terms:
Let’s try
to run this, and if an exception is about to happen, catch
it and do this instead.
For example
Imagine you’re building a calculator that divides two numbers. Everything works fine until someone enters zero as the denominator and it crashes the whole thing:
public class NoTryCatch {
public static void main(String[] args) {
int result = 10 / 0; // Error: Division by zero!
System.out.println("This will never run.");
}
}
Here we can see that Java detects the division by zero, knows this wont work, and so throws an ArithmeticException
, and shuts the program down immediately.
The problem of course is that the user has no idea what went wrong because our warning of "This will never run."
never even gets printed! The program crashing stops them from knowing about their error.
Now imagine this happening in an online banking app. If a transaction fails, you don’t want the entire system to crash or the user to think their money is lost.
Instead, you want the program to catch the error and handle it properly, like so:
public class TryCatchExample {
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println("This won't run.");
} catch (ArithmeticException e) {
System.out.println("Oops! You can't divide by zero.");
}
System.out.println("Try again.");
}
}
This time, instead of letting the program crash, Java jumps to the catch
block, where we handle the error by printing "Oops! You can't divide by zero."
After handling the exception, execution moves past the catch
block, and "Try again"
is printed. That’s a huge difference.
Now, the user at least knows what went wrong instead of being left in the dark. More importantly, the program keeps running so they can try again.
Bear in mind that this is actually not a situation that requires try and catch. This is merely to illustrate the mechanism. The ArithmeticException is actually an unchecked exception. If it were a checked exception we would have to handle it.
This is the power of try
and catch
. Errors will happen and you can’t stop that. But what you can do is decide how your program should respond when they do.
Now that you understand the goal of try
and catch
, let’s go deeper and look at some other situations, such as what would you do if multiple things can go wrong at once?...
Errors rarely happen in isolation. A single block of code can fail in multiple ways, depending on the input, the environment, or just bad luck.
For example
Let’s say a program divides numbers and accesses an array.
Both can go wrong. Java throws the first error it encounters and immediately stops execution. The second error never even occurs. How would we handle that, we could do something like this:
public class NoTryCatch {
public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
int result = 10 / 0; // Error: Division by zero!
System.out.println(numbers[5]); // Error: Out of bounds!
} catch (Exception e) {
System.out.println("Problem: " + e.getMessage());
}
}
}
Since 10 / 0
is an invalid operation, Java throws an ArithmeticException
and crashes. That means the next line — System.out.println(numbers[5]);
— never runs. However, if we didn’t have the first problem, we would have had the ArrayIndexOutOfBoundsException. Both of these are exceptions and would be handled by our catch block.
Why does this matter?
If we can have multiple errors, we might want to handle them differently. That is why multiple catch blocks are great. They tell us specifically what went wrong and can take precise action.
So how do we do this?
We can handle both exceptions separately by using multiple catch
blocks.
public class MultipleCatchExample {
public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
int result = 10 / 0; // ArithmeticException
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException
} catch (ArithmeticException e) {
System.out.println("Math error: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Invalid array index!");
}
}
}
Now, Java detects the first error (10 / 0
) and jumps to the corresponding catch
block, printing:
Math error: / by zero
Since an exception stops execution inside the try
block, the second line (numbers[5]
) never runs - just like before.
The difference? The program now has precise error handling when it goes wrong. And we know which error was thrown. Also, anything that we can’t really imagine at this point, such as a NullPointerException in that block, won’t be caught anymore either.
For example
Let’s say we change 10 / 0
to 10 / 2
, so the division no longer fails.
public class FixedFirstError {
public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
int result = 10 / 2; // Fixed: No more division by zero
System.out.println(numbers[5]); // Still causes an error!
} catch (ArithmeticException e) {
System.out.println("Math error: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Invalid array index!");
}
}
}
Now that division works, Java continues execution and reaches numbers[5]
, which is out of bounds. This time, it throws an ArrayIndexOutOfBoundsException
, which is caught by the second catch
block, printing:
Invalid array index!
That’s exactly why multiple catch
blocks exist — so we can handle each potential failure individually rather than dealing with all of them in the same way.
So far, we’ve seen how try
and catch
handle exceptions and prevent your program from crashing. But what if you always need to run some code no matter what happens?
Let’s say your program opens a file, reads its contents, and then closes it. If the file is missing, an error occurs. But whether or not there’s an error, you still need to close the file to prevent resource leaks.
Well, that’s where the finally
block comes in.
The finally
block runs after the try
block - no matter what happens inside it. Whether an exception is thrown or everything works perfectly, the code inside finally
always executes. (Well… Almost always, except if you call something like System.exit(0); in the catch block, which would not be a very common requirement.)
For example
Here we can use finally
to close a scanner object that’s reading a file.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class FinallyExample {
public static void main(String[] args) {
Scanner scanner = null; // This has to go outside of the try for scope in the finally block
try {
File file = new File("data.txt");
scanner = new Scanner(file);
System.out.println(scanner.nextLine());
} catch (FileNotFoundException e) {
System.out.println("File not found.");
} finally {
if (scanner != null) {
scanner.close();
System.out.println("Scanner closed.");
}
}
}
}
What’s happening here?
try
block attempts to open the file and read its first lineFileNotFoundException
, and the catch
block prints "File not found"
finally
block runs no matter what happens and closes the scanner to free up system resourcesEven if an error occurs, the scanner will always be closed.
Why is this useful?
Simply because many situations require cleaning things up after running some code, including:
Without finally
, you’d have to repeat cleanup code in both the try
and catch
blocks — which is inefficient and easy to forget. But finally
ensures that cleanup always happens, whether an error occurs or not, making your program more reliable and efficient.
Sidenote: We won’t deal with the concept of try-with-resources until the end of this beginner guide, but often resources are
autocloseable
. This means that they can be automatically closed with a special try syntax, eliminating the need for afinally
block. You’ll see this soon! Just keep reading.
return
statement?One useful thing to know is that finally
always runs, even if a return
statement is used inside try
or catch
.
For example
public class FinallyWithReturn {
public static void main(String[] args) {
System.out.println(testMethod());
}
public static String testMethod() {
try {
return "Try block executed.";
} finally {
System.out.println("Finally block executed.");
}
}
}
Even though the try
block returns a value, Java doesn’t exit the method until the finally
block finishes running. That means this prints:
Finally block executed.
Try block executed.
This guarantees that cleanup code inside finally
always executes, no matter what.
Now that we’ve covered try
, catch
, and finally
, let’s look at some common mistakes developers make when handling exceptions, and how to avoid them.
Using try
and catch
is essential for handling errors in Java, but they’re easy to misuse. If you don’t handle exceptions properly, you could hide real bugs, make debugging harder, or even introduce new issues.
Let’s go through the most common mistakes developers make — and how to do things the right way.
One of the biggest mistakes Java developers make is catching exceptions without actually handling them. Just because your program doesn’t crash doesn’t mean it’s running correctly.
For example
Some developers wrap their code in a try-catch
block but don’t take action when an error occurs. Instead, they leave the catch
block empty or print a vague message that doesn’t help with debugging.
public class BadExample {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (Exception e) {
// Doing nothing here!
}
System.out.println("Program continues...");
}
}
At first glance, this might seem fine because the program doesn’t crash. But here’s why this is a bad idea:
Imagine this happening in a financial application. If an error occurs while processing a payment but gets silently ignored, users might never know their transaction failed and you can later not find out why.
How to fix it
If you catch an exception, make sure you’re actually handling it. That could mean logging the error, notifying the user, retrying an operation, or gracefully shutting down — but never ignoring it completely.
At the very least, log the error so you know what happened:
public class BetterExample {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero occurred.");
e.printStackTrace(); // Prints full error details for debugging
}
}
}
In this example:
e.printStackTrace();
method prints the full error details, making it easier to trace the problem. In real applications, this is probably replaced with actual loggingAt first, catching every possible exception with Exception
might seem like a good safety net. After all, it ensures the program doesn’t crash unexpectedly, right?
But in reality, catching all exceptions this way hides specific errors and makes debugging much harder.
For example
public class BadGenericCatch {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (Exception e) { // Too broad!
System.out.println("An error occurred.");
}
}
}
Why is this bad?
NullPointerException
), you might never realize itSo instead of catching everything, you should catch only the exceptions you expect.
public class BetterSpecificCatch {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Math error: " + e.getMessage());
}
}
}
"Math error: / by zero"
is far more helpful than "An error occurred"
try
and catch
for unchecked exceptions instead of fixing the root causeUnchecked exceptions happen when something is wrong in your code—like trying to access a null
value. Unlike checked exceptions (which result from external problems like missing files), unchecked exceptions signal a bug that should be fixed, not caught.
But some Java developers misuse try and catch as a shortcut. Instead of fixing the actual problem, they wrap the code in a try-catch block to stop the program from crashing. This hides the real issue, making debugging harder and leading to unpredictable behavior.
For example
Bad practice: Catching NullPointerException
instead of preventing it
public class WrongUsage {
public static void main(String[] args) {
try {
String text = null;
System.out.println(text.length()); // This throws NullPointerException
} catch (NullPointerException e) {
System.out.println("Oops, something went wrong.");
}
}
}
At first glance, this might seem like a quick fix—the program doesn’t crash. But in reality, this is just covering up a deeper issue instead of solving it.
The real issue isn’t the exception—it’s that text
is null
in the first place.
Catching a NullPointerException
doesn’t fix anything — it just prevents the program from visibly failing. So if you rely on try-catch here, you’re hiding a logic error instead of addressing it properly.
How to fix it
Instead of catching the error, check if the object is null
before using it:
public class CorrectUsage {
public static void main(String[] args) {
String text = null;
if (text != null) {
System.out.println(text.length());
} else {
System.out.println("Text is null, can't get length.");
}
}
}
Now, instead of catching an exception, we prevent it from happening in the first place.
try
and catch
when an if
statement is betterSome Java developers overuse try-catch for handling predictable situations, like checking user input. While exception handling is great for unexpected failures, it’s not the best tool for normal program logic.
If you can check for a condition before an error happens, that’s almost always better than catching an exception after the fact.
For example
Bad practice: Using try-catch
for input validation
public class BadValidation {
public static void main(String[] args) {
String input = "abc";
try {
int number = Integer.parseInt(input);
System.out.println("You entered: " + number);
} catch (NumberFormatException e) {
System.out.println("Invalid input!");
}
}
}
At first glance, this might seem like a reasonable way to catch bad input—if the user enters something that’s not a number, the program won’t crash.
But this approach is inefficient. Why? Because the program already knows input might be invalid — it should check before attempting to convert it.
Why is this bad?
try-catch
blocks are slower than simple condition checks, especially if used repeatedly in loopsHow to fix it
try-catch
should be your last resort, not your first choice. If you can prevent an error before it happens (like validating user input), it’s always a better approach. Use try-catch
for true unexpected failures, not normal program flow.
For example
Instead of waiting for an exception, check if the input is valid before converting it:
public class CorrectValidation {
public static void main(String[] args) {
String input = "abc";
if (input.matches("\\d+")) { // Checks if input contains only digits
int number = Integer.parseInt(input);
System.out.println("You entered: " + number);
} else {
System.out.println("Invalid input! You should have entered a number.");
}
}
}
Now, we only try to parse the number if we know it’s valid.
finally
when you don’t need to)Earlier, we covered how the finally
block ensures cleanup happens no matter what. But a common mistake many Java developers make is forgetting to clean up resources altogether — especially when working with files, databases, or network connections.
If you don’t close a resource after using it, your program can leak memory, slow down, or even crash. It might not be obvious right away, but over time, unclosed resources build up, consuming system memory and causing unpredictable behavior.
For example
A typical mistake is opening a resource inside a try
block but not closing it in the finally
block.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class BadFileHandling {
public static void main(String[] args) {
Scanner scanner = null;
try {
scanner = new Scanner(new File("data.txt"));
System.out.println(scanner.nextLine());
scanner.close();
} catch (FileNotFoundException e) {
System.out.println("File not found.");
}
// Scanner never gets closed!
}
}
At first glance, this might not seem like a big deal. But if an error occurs before scanner.close()
is called, the scanner stays open. When this happens repeatedly — say, in a loop or a long-running process — resources pile up, memory usage spikes, and performance suffers.
try-with-resources
insteadSince Java 7, try-with-resources
is the best way to manage resources automatically. Instead of worrying about closing resources manually, you let Java handle it for you.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File("data.txt"))) {
System.out.println(scanner.nextLine());
} catch (FileNotFoundException e) {
System.out.println("File not found.");
}
// No need for a finally block—Java automatically closes the scanner.
}
}
This approach ensures the scanner is always closed as soon as the try block exits, whether an exception occurs or not. You don’t need a finally
block, and there’s no risk of forgetting to clean up resources.
finally
?try-with-resources
is the best way to handle autoclosable resources like files and database connections. But there are cases where finally
is still useful.
(Some objects don’t implement AutoCloseable
, meaning they can’t be used inside try-with-resources
. If that’s the case, finally
is your fallback option).
It’s also useful when you need to clean up non-resource-related tasks, like resetting global variables or writing log messages.
For example
Here’s how finally
can be used as a backup solution:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class FinallyExample {
public static void main(String[] args) {
Scanner scanner = null;
try {
scanner = new Scanner(new File("data.txt"));
System.out.println(scanner.nextLine());
} catch (FileNotFoundException e) {
System.out.println("File not found.");
} finally {
if (scanner != null) {
scanner.close();
System.out.println("Scanner closed.");
}
}
}
}
Here, even if an exception is thrown, the finally
block ensures the scanner gets closed properly. It’s not as clean as try-with-resources
, but it’s better than leaving resources open.
Forgetting to clean up resources is a common mistake that can lead to slowdowns, crashes, and memory leaks.
So:
try-with-resources
whenever possible. It's cleaner, safer, and removes the risk of forgetting to close things manuallytry-with-resources
isn’t an option, then a finally
block is your next best choice. It guarantees that cleanup happens, even if an error occursNow that you understand try
, catch
, and finally
, the best way to learn is to experiment.
Write a program, force errors to happen, and see how different exceptions behave. Try catching specific errors, using multiple catch
blocks, and adding a finally
block to clean up resources.
Fix logic errors instead of just catching them, and always handle exceptions in a way that makes debugging easier.
The more you practice, the better you'll get at writing stable, error-proof Java code.
Now, go break some code — so you can fix it!
Remember, if you want to fast-track your Java knowledge and get as much hands-on practice as possible, then check out my Java programming bootcamp:
Updated for 2025, you'll learn Java programming fundamentals all the way from complete beginner to advanced skills.
Better still, you’ll be able to reinforce your learning with over 80 exercises and 18 quizzes. This is the only course you need to go from complete programming beginner to being able to get hired as a Backend Developer!
Plus, once you join, you'll have the opportunity to ask questions in our private Discord community from me, other students and working DevOps professionals.