Phil Tayco Slide version 1.0 Created Nov. 26, 2017 Error Handling Phil Tayco Slide version 1.0 Created Nov. 26, 2017
Error Handling Up to this point, we have developed code that assumes incoming data is relatively error free These are reasonably safe assumptions to make since the main test programs have been simple and intended to confirm the logic and design of the classes Advanced programs have more sophisticated means at obtaining data beyond the simple command line Such user interaction can lead to considering how to handle errors with the data as it is received to process We now consider how to deal with and design potential issues with data as appropriate
What time will the sun rise? Software Testing What exactly is software testing? Typical considerations for this is ensuring that the program you are writing “doesn’t break” Defining that is a challenge that quickly becomes an impossibility as programs get more complex To illustrate, take a simple question: What time will the sun rise?
Software Testing Answering the question started out as straightforward but as more unforeseen details arose, refinement of the question became necessary Eventually we reached a point where we reached an acceptable level of detail for the question for answering the question Further refinement may have been done but we decided on a stopping point because more details were either unnecessary or perhaps too costly to address Even the simplest of questions can become challenging to answer and can be impossible to do so in a “foolproof” way Error handling in software follows that same line of thought
Software Testing Think of the “what time will the sun rise” question as requirements for your software As requirements are defined, refining them to an appropriate level of detail becomes an important process Just as important is the recognition that a perfect level of requirements definition is an impossible task Refinement is often necessary due to an unforeseen consideration at the time the requirement was drafted As we learn more about what we want the program to do, requirement updates become inevitable – it is impossible to foresee everything When do we stop refining? When it becomes more costly than beneficial to define the requirement and how we want to handle it
Software Testing As such, software testing, then, cannot be defined as “making sure the software doesn’t break” While that is an important factor to achieve and consider, making sure the program doesn’t break must be stated in terms that are acceptable and cost-effective to the project Since a program breaking involves situations that may not be foreseen, we must then define software not breaking in foreseeable terms Such terminology is best specified in requirements - “The software shall not break” becomes “The software shall handle numeric and alphabetic input” This leads to a concrete definition of what software testing is…
Software Testing “Ensuring the requirements are being met” While acknowledging that we want to do our best that the software doesn’t break, we also make it more cost effective to state that we will at least make sure the requirements we can foresee can be met This allows for a systematic and measurable approach to evaluating software readiness – if someone says “the software is 80% ready to go”, that can directly translate to the amount of requirements being met Requirements can also be prioritized allowing us to further evaluate how well the product development is going
Software Testing This is all interesting theoretical information on the fundamentals of software testing, but how does that apply to OO Programming? OO design is all about putting an infrastructure in place that deals with change in a cost effective manner Software testing through explicit requirements definitions help pave the way to designing classes that not only meet current requirements, but set a foundation for dealing with inevitable change Such requirements also help define what type of error handling we know we need (versus putting in code to handle errors in the “in case the user does this” category)
Error Handling Consider the following. There are at least 2 potential run-time errors that can occur here: public static int findQuotient(int n, int d) { return n / d; } public static void main(String[] args) Scanner input = new Scanner(System.in); System.out.print("Enter numerator: "); int n = input.nextInt(); System.out.print("Enter denominator: "); int d = input.nextInt(); int q = findQuotient(n, d); System.out.println("Quotient is: " + q);
Error Handling The input is expecting to look for integers in the input stream If the user enters alphabetic characters such as “six”, an “InputMismatchException” run time error will occur Also, if the user enters integers for n and d, but d is zero, a “ArithmeticException” run time error will occur These are events triggered by the user synchronously, meaning they can occur at specific lines of code being executed (“nextInt” or “n / d” for examples) As such, they can be handled with if statements in the code That works for the division by zero, but nextInt is a bit harder since that is code written for us by Java (the issue occurs within nextInt which we cannot see)
Error Handling An alternative viewpoint is to look at the areas where a run time error can occur: public static int findQuotient(int n, int d) { return n / d; } public static void main(String[] args) Scanner input = new Scanner(System.in); System.out.print("Enter numerator: "); int n = input.nextInt(); System.out.print("Enter denominator: "); int d = input.nextInt(); int q = findQuotient(n, d); System.out.println("Quotient is: " + q);
Error Handling The lines of code in red are areas where these run time errors can occur. Java allows us to “wrap” this code This wrapping allows us to write code that says “here’s some code that may result in a run time error. If that happens, here’s how to handle it” try { System.out.print("Enter numerator: "); int n = input.nextInt(); System.out.print("Enter denominator: "); int d = input.nextInt(); int q = findQuotient(n, d); System.out.println("Quotient is: " + q); }
Error Handling The “try” block wraps the code that tells Java there may be run time errors that can occur here. These errors are called “Exceptions” If an Exception occurs, an Exception object is generated and sent to another block of code after the “try” block called the “catch” block catch (InputMismatchException e) { System.out.println(“Input error. Please use only integers”); } The catch block is entered only if an Exception is generated within a try block
Error Handling Multiple catch blocks can be chained if the try block can generate multiple types of Exceptions catch (InputMismatchException e) { System.out.println(“Input error. Please use only integers”); } catch (ArithmeticException e) System.out.println(“Error with numbers”); The key to note here is that these are errors we have decided to handle. We are not putting these “in case the user enters bad data”, we are putting them to “ensure only handling integer data and denominators not equal to 0”
Error Handling Notice for the control flow that if an Exception is caught, code execution continues from there – it’s like a “go to” from the area in the try block where the Exception was found to the appropriate catch block where the Exception is caught Once the catch is done, normal program flow resumes after the catch blocks In this example, the effect is an error is handled, but the program ends from there As would normally be handled, keeping the program running if an Exception is found will involve using a loop – the key is putting the loop around the entire try…catch block
Error Handling do { try System.out.print("Enter numerator: "); int n = input.nextInt(); System.out.print("Enter denominator: "); int d = input.nextInt(); q = findQuotient(n, d); continueFlag = false; } catch (InputMismatchException e) System.out.println("Incorrect input error" ); input.nextLine(); catch (ArithmeticException e) System.out.println(e); } while (continueFlag == true); System.out.println("Quotient is: " + q);
Error Handling Here, the do…while loop surrounds the try…catch blocks controlling whether or not the process of collecting evaluating user data is correct The “continueFlag” variable (declared before the loop starts) manages the loop – the loop is set to continue until all the code in the try block is executed Notice in the InputMismatchException catch block, there’s an extra “nextLine()” function – this is due to input Scanner object still having a carriage return in its buffer that needs to be cleared so it can read the next line of input (a common issue with String input through the Scanner) Remember this does not mean the code is now foolproof – it only means we have code designed to capture Exceptions we designed to handle
The Exception Class The Exception class is actually a superclass in the Exception inheritance hierarchy “InputMismatchException” and “ArithmeticException” objects are types of Exceptions Because they inherit from Exception, it then allows for any catch block that the type of object thrown from a try block is also a type of Exception Put another way, this allows catch blocks to also use a sort of “default” way of catching Exceptions similar to the default case in a switch statement
Error Handling catch (InputMismatchException e) { System.out.println(“Input error. Please use only integers”); } catch (Exception e) System.out.println(“Exception: ” + e); Here, the last possible type of Exception that can be caught is the superclass Exception object Any type of Exception that gets thrown in a try block now has a “default” catch block if any of the other catch blocks do not catch the type thrown In this example, if an unforeseen Exception such as “RunTimeException” is thrown, the last catch block will catch it polymorphically
The Exception Class The default catch Exception block is useful in case an unforeseen Exception is thrown As good practice, since you are already setting up the code infrastructure to do a try…catch set, it is helpful to have a last catch block for the generic Exception class as a last resort try…catch should still be used to identify catching known Exceptions. Default Exceptions are best used for backup purposes
Summary The text goes into more detailed extent on the use of try…catch as well as another clause, “finally” Exceptions are also classes so it is possible to define your own Exception subclasses Methods can also be designed to “throws” specific Exceptions Best practice is to try and define through requirements what types of issues you want your program to handle and use code (try…catch or if…else statements) to address them try…catch is nice because it can structurally handle multiple potential issues in a set of code Use exception handling as often as possible but only do so as required for as much as one can foresee