Understand Exception Handling
Understand Exception Handling
Testing can catch most of the common errors within programs. Take the scenario of a software for an ATM machine. Edge cases like incorrect PIN, customer trying to retrieve more money than what’s available in his account etc. will get included in the tests. What happens with scenarios that go untested? (eg: Customer tries to dispense an amount larger than the money remaining in the ATM). In this case, the ATM machine shouldn’t just break apart and shut down due to an error. The software should either handle issues like these in a meaningful way or set up graceful termination of the program.
Assuming errors won’t occur and not caring about error scenarios can have a serious impact. Also, the cost to rectify errors once the software is deployed can be significantly higher.
Exception Handling is how computer programs improvise or recover from unexpected situations without going down or ending up in an unrecoverable state. Exception handling ensures graceful handling of errors.
Understand the need for handling exceptions
Understand exception handling in Java
Understand classification of exceptions in Java
Understand handling multiple exceptions at once
Understand cleaning up on exceptions
Understand Exception Handling
Testing can catch most of the common errors within programs. Take the scenario of a software for an ATM machine. Edge cases like incorrect PIN, customer trying to retrieve more money than what’s available in his account etc. will get included in the tests. What happens with scenarios that go untested? (eg: Customer tries to dispense an amount larger than the money remaining in the ATM). In this case, the ATM machine shouldn’t just break apart and shut down due to an error. The software should either handle issues like these in a meaningful way or set up graceful termination of the program.
Assuming errors won’t occur and not caring about error scenarios can have a serious impact. Also, the cost to rectify errors once the software is deployed can be significantly higher.
Exception Handling is how computer programs improvise or recover from unexpected situations without going down or ending up in an unrecoverable state. Exception handling ensures graceful handling of errors.
Understand the need for handling exceptions
Understand exception handling in Java
Understand classification of exceptions in Java
Understand handling multiple exceptions at once
Understand cleaning up on exceptions
~/workspace/bytes/
directory and cd
to it
mkdir -p ~/workspace/bytes/
cd ~/workspace/bytes/
~/workspace/bytes/
directory from here using one of the following commands:
git clone https://gitlab.crio.do/crio_bytes/me_exception_handling.git
git clone git@gitlab.crio.do:crio_bytes/me_exception_handling.git
javac App.java
java App
Exception handling, at first, might seem like an additional burden. However, the real benefit kicks in when the project becomes large and it becomes hard to "remember" all your code. This is also the case when you are working as a team and different components of the project will be written by multiple developers, who can all make changes independently.
Let’s look at an example to see the value of this.
What would you feel if you saw this screen when you eagerly typed in some query on Quora?
For someone who has seen a lot of error traces and loves this, the next step would be to go through the adventure of figuring out what the error is saying. But, for a layman user, this won’t make any sense and the user will leave the page. Not only did the user not get what they wanted, but was also not given any information to figure out what went wrong.
How would the user emotion be if the screen was like this?
Here, the user is shown a much more "usable" message that communicates what happened. Users would appreciate this and try out the suggestions mentioned here.
Exception Handling allows developers to recover from such errors and handle it gracefully with a clear message to users.
The images are only representative and not real website images from Quora
Though digging into stack traces and finding the root cause of an error will be exciting sometimes, you wouldn’t want to deal with it on a frequent basis. Debugging code takes time and effort.
With any codebase that you work on, it is difficult to debug an error by remembering all the functionality of different classes or how they interacted with each other. Proper utilisation of exception handling procedures can save you a lot of debugging time by including more information when an error is seen.
The above error trace tells you a MismatchedInputException
occurred due to so and so reason, as well as the exact location of code where it happened. You cannot be sure, without taking a look at the code, as to what the methods were doing. Then, you need to come up with a hypothesis, do some checks and figure out the exact reason.
What if the error screen was something like this?
The exact context in which the error occurred (when reading a file), cause of the error (cannot deserialize a String) and possible solution (validating the file format) is already there for you. Though, a level of detail like this may not be possible for all scenarios, logging the parameter values and other additional information will help developers debug your application much faster and with less effort.
By going the extra mile of adding some additional information when developing the project you are doing a favor to yourself and to anyone else maintaining the project.
Let’s get to know the files we have in the project that we fetched during the Setup task. The logic is implemented to authenticate users. This is done by checking the username and password they entered against a data source. Application has functionality to perform authentication both when the user is offline (using LocalStorage.java which gets username and password data from a local file) and online (using NetworkStorage.java which fetches this information from a site on the cloud)
App.java - contains logic to check if username and password matches
loginOffline(username, password)
- use locally saved (file based) data to authenticate user
loginOnline(username, password)
- use data saved on cloud to authenticate user
main()
- serves as the frontend (for now) where users will input username and password data
LocalStorage.java - performs storage operations and fetches login data from UnameToPass.txt file saved locally
getPassword(username)
- fetches password for a particular username
getUsernameToPasswordMapping()
- fetches all username to password mappings from the UnameToPass.txt file
NetworkStorage.java - similar functionality as LocalStorage.java, but fetches data over the Internet. (We’ll look at this in the later milestones)
The main()
method of the App
class is calling its loginOffline(username, password)
method with parameters Dhoni and dhoni**Password. The methods provided by the LocalStorage
class is used to fetch the password corresponding to a username. The loginOffline()
method checks if the passwords match and returns a message to the user accordingly
LocalStorage localStorage = new LocalStorage();
public String loginOffline(String username, String inputPassword) {
String expectedPassword = localStorage.getPassword(username);
if (expectedPassword.equals(inputPassword)) {
return "Login successful";
} else {
return "Invalid password!";
}
}
public static void main(String[] args) {
App app = new App();
System.out.println(app.loginOffline("Dhoni", "dhoniPassword"));
}
The UnameToPass.txt file contains the username-password mapping which the LocalStorage
class utilizes. Check it, does username, Dhoni map to password, dhoni**Password? Try compiling and running the App.java file.
We see an error!
Let’s understand the error printed out. It’s trying to tell us there are a couple of exceptions, FileNotFoundException
and IOException
, which are not caught. We must either catch them or declare the methods to throw these exceptions. The lines causing the error are in the LocalStorage.java file. This file also gets compiled when compiling App.java as it’s used within it.
You’ll see VS code underlining the exact lines where the compilation error occurs, in red. Hover over these and you’ll find that the class/method declaration has an extra throws
keyword followed by one of the exceptions we saw earlier. To resolve the compilation error, you’d remember from the error seen earlier that the options were
Catch the error OR
Declare exceptions to be thrown.
As you’d have guessed, throws
keyword is used to say that a class/method can throw an exception. This leaves the responsibility of dealing with the exception (FileNotFoundException
) to the method that uses the FileReader
class. Here, the getUsernameToPasswordMapping()
method has to deal with it.
So, the getUsernameToPasswordMapping()
method has two options.
Throw the exception using throws
so that the exception needs to be handled by any other function calls getUsernameToPasswordMapping()
Catch the exception and handle it.
Let’s explore both the methods one by one.
TODO: Modify getUsernameToPasswordMapping()
to handle the exceptions by throwing both the exceptions. Compile the App.java file again. Does it show errors at lines in getUsernameToPasswordMapping()
like before or in a different method?
public Map<String, String> getUsernameToPasswordMapping() throws FileNotFoundException, IOException {
Similarly, you’ll see the same issue for the getPassword()
method as it’s using the getUsernameToPasswordMapping()
method.
TODO: Modify getPassword()
to throw the exceptions it needs to handle like we did with getUsernameToPasswordMapping()
.
With the exceptions declared to be thrown in both the methods, you’ll see that the red lines vanished. What will happen on compiling the App.java class now? Think, then act.
Compiler is now complaining about not catching or declaring the exceptions in the App.java file. The error is due to using the getPassword()
method of the LocalStorage
class which we declared to throw exceptions in the previous section. With what we have learnt till now, regarding exception handling, we can use the throws
to handle exceptions. Ensure App.java compiles by throwing the exception like we did for the other two methods before. Run it.
crio-user@crio-demo:$ javac App.java
crio-user@crio-demo:$ java App
Login successful
crio-user@crio-demo:$
The loginOffline()
method is user-facing (which we’re simulating using the main()
method for now), meaning that the user gets a message "Login Successful" in this case.
That’s good, we have handled the Exception successfully. Or, have we?
What would the user see if loginOffline()
causes an exception? (Remember we declared it to throw exceptions).
getUsernameToPasswordMapping()
reads from, in the LocalStorage
classThis is exactly what we had discussed in Milestone 1. It is not helpful for the users to see this.
If you recall, the compiler had given you two options to deal with the exceptions earlier. We’ve seen the "throws" option. Now, let's explore the second option which is "catch". Catching an exception is where we define beforehand how we want to recover when an exception occurs. Java provides the try-catch statement for that. The part of the code that can cause an exception is wrapped within the try
clause and how we want to recover from an exception is wrapped within the catch
clause.
(Remove any throws
keyword usage in App.java, so we can explore this catch
option)
TODO: Use the try-catch statement to catch IOException
inside the loginOffline()
method (IOException
is the parent class of FileNotFoundException
. So, catching FileNotFoundException
won’t be required after catching IOException
)
What does the user see when there’s an error now? Compile and execute to find out. Isn’t it a much better message for the user?
For the developers to have debugging information whenever the exception occurs, we can use the printStackTrace()
method of the exception object and configure it to be logged to a log file. This will provide information about where exactly the exception occurred.
TODO: Use the debugger to check the flow of the program
In the normal case
When there’s an exception
Set breakpoints on these lines for that
expectedPassword = localStorage.getPassword(username);
in App.java
unameToPassMap = getUsernameToPasswordMapping();
in LocalStorage.java
FileReader fileReader = new FileReader(loginDataFile);
in LocalStorage.java
(Hint: Set a breakpoint inside the catch
block and click Debug on top of main()
in VS code)
Don’t forget to reset the file name to the correct one, in the LocalStorage class
FileNotFoundException
and IOException
were thrown in the LocalStorage
class methods. Try removing FileNotFoundException
and compile again. Does the compiler complain now?We looked at how to handle exceptions in the previous milestone.
However, there are scenarios like the server running out of memory or the case of stack overflow which the application shouldn’t try to catch, since these are concerned with the system itself rather than being an issue with the application. Unfavourable situations like the are Error
s in Java and are used by the Java Virtual Machine (JVM). Examples are OutOfMemoryError
and StackOverflowError
.
Throwable
is the parent class for both the Error
and the Exception
classes. Both Error
and Exception
classes have their own hierarchy of errors and exceptions.OutOfMemoryError
and StackOverflowError
are sub-classes of the Error
class
Exceptions like we saw earlier are also unexpected events like a file not being found, error during file I/O operation. But here, it’s possible for the application to recover, this is the main difference between Errors and Exceptions. IOException
subclasses the Exception
class.IOException
class is again the parent class for other exceptions like the FileNotFoundException
. This was why the compiler didn’t throw an "unreported exception" error even if we only caught the IOException
class inside the loginOffline()
method.
Exceptions in Java fall into two categories - Checked and Unchecked. Checked exceptions must either be declared to be thrown or caught. These are "checked" by the compiler and it throws an error if we fail to do that. This is what happened when we ran the App
class earlier. We didn’t get to run the class but failed at the compilation stage itself.
TODO: Try creating a new method in the App
class which uses the localstorage.getPassword()
method and see if it compiles.
Note: Comment out the new method you just created or ensure you catch the Exceptions inside it
Unchecked exceptions aren’t checked by the compiler. For instance, NullPointerException
is a common exception caused when we try to call some methods on an object with null
value. These usually signify programming errors and hence is a good practice not to catch them.
TODO: Change the username in main()
method from Dhoni to your name. Compile and run the App
class now. What happened?
In LocalStorage::getPassword()
, the get()
method provided by the Map
class returns null
if the key isn’t present. Here, using the equals
method on the null
object inside loginOffline
causes NullPointerException
.
TODO: Use the debugger to check the program flow in this scenario. Does control reach the catch
block? Add an additional check to see if the returned value is null
and return an appropriate message to the user to handle this cleanly.
All subclasses of the RuntimeException
class are unchecked, so are that of the Error
class.
The loginOffline()
method throws FileNotFoundException
when it can’t find the file locally to read login information from. A new requirement has come in to improve the handling in this case. The NetworkStorage
class methods are to be used to fetch the login information from the cloud server. A loginOnline()
method has been provided for this purpose.
Java allows multiple catch
blocks along with a try
block where we can perform separate actions for different exceptions.
TODO:
Use multiple catch statements inside the loginOffline
method.
Uncomment the loginOnline()
in App.java
First catch
the FileNotFoundException
and recover from this exception by calling the loginOnline()
method.
Use a second catch
statement for the IOException
and handle as before.
Change the input file name in the getUsernameToPasswordMapping()
method to cause a FileNotFoundException
and confirm that the application runs successfully for this error case.
try {
expectedPassword = localStorage.getPassword(username);
} catch (FileNotFoundException exceptionObject) {
// log error trace to file
// exceptionObject.printStackTrace();
expectedPassword = loginOnline(username, inputPassword);
} catch (IOException exceptionObject) {
// log error trace to file
// exceptionObject.printStackTrace();
return "Internal error, please try again later";
}
Use the debugger to see the program flow. Answer the following questions
When the FileNotFoundException
occurs, are all of the catch
blocks entered? (alter the input file name in getUsernameToPasswordMapping()
to cause the exception)
The throw
keyword can be used to explicitly throw an Exception. Use it to throw an IOException
inside the try block in loginOffline()
method. As IOException
is the super-class of the FileNotFoundException
class, does the control go inside the first catch block?
IOException
is caught first and then the FileNotFoundException
?Inspect the getUsernameToPasswordMapping()
method in NetworkStorage
class. A BufferedReader
object is created and also closed there (see close()
method). It is good practice to always release any system resources like allocated memory, open file handlers etc. after their usage. Releasing resources is an example of cleanup.
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.openStream()));
Uncomment the commented out section in the main()
method and run it (Hit Ctrl + c
when you start seeing errors). By uncommenting these lines, main()
now calls the loginOnline
method concurrently, a large number of times. (Tip: To uncomment a block of code, highlight the lines and hit Ctrl + /
)
The error was caused due to an IOException
and the error message tells that the server returned a HTTP 429 response code. What does this response code mean? Why do you think it occurred?
The point of failure in the code is inside the getUsernameToPasswordMapping()
method and on checking the exact line causing the error, you’ll find that it’s the line where we create a new BufferedReader
instance. Find out what caused this error by hovering over each keyword in that line and see if any of them throws an IOException
Checking the program’s flow when an exception occurs, we see that the IOException
would return the control from the getUsernameToPasswordMapping()
and won’t run the code that comes after the code that caused the Exception. This means the BufferedReader
instance we created won’t be closed.
Java provides the finally
block to wrap code which should get executed irrespective of an exception occuring or not. The finally
block can be used as part of a
try-finally block → wrap code that can throw exception inside try
and include code that must be executed always in finally
try-catch-finally block → the try-catch block (e.g. like we used inside loginOffline()
) gets an additional finally
section which will always get executed
TODO:
Use finally
block inside getUsernameToPasswordMapping()
to ensure the BufferedReader
resource is closed even if an exception occurs (do this for both the LocalStorage
and NetworkStorage
classes)
Use debugger to understand program flow
Is the finally
block visited when there’s no exception thrown?
Is the finally
block visited only for IOException
? (Change the URL to something random)
finally
block for ensuring a resource is closed can be easy. Is there some way to ensure any open resource gets automatically closed after its usage is over?We saw why handling exceptions are critical from both a user’s perspective and a developer’s perspective.
Exceptions can be either caught or declared to be thrown in Java
The try-catch block is used for catching exceptions
The throws keyword is used along with a class/method declaration to denote that it throws an Exception
Java has two higher level classes, Exception and Error to denote unexpected scenarios that occur during the run of the program. Both of these inherit the Throwable class
Exceptions are usually handled by the programmer
Errors are caused due to system errors and hence not handled
Exceptions that the compiler checks for are called Checked Exception. These are the ones that must be caught or declared to be thrown or otherwise the code won’t compiler
Exceptions that subclasses the RuntimeException class isn’t checked by the compiler and hence called Unchecked Exception
Find the
Best Practices
Handle all checked exceptions
Close or release resources in the finally
block
Always provide meaningful message on Exception
Avoid empty catch
blocks
Further Reading
Explain why exception handling is important
Provide better user experience to the product users
Provide better debugging experience for developers
Handle exceptions in different scenarios for Java code