Training: Exception Handling in Java SE 17
Table of Contents
- Introduction
- The role of exceptions
- The try/catch blocks
- Cleanup management with finally
- Implementation of try/catch/finally — Demo CalcEngine (before)
- Cleanup automation — AutoCloseable and try-with-resources
- Transition to automatic cleaning — Demo CalcEngine (after)
- Module 2 Summary
- Introduction
- The hierarchy of exception classes
- Exception handling by type — Multiple catch blocks
- Checked vs unchecked exceptions
- Handling multiple exceptions — Demo CalcEngine
- Handling unchecked exceptions — Demo CalcEngine
- Exceptions and methods — Traversal of the call stack
- Handling exceptions between methods — Demo CalcEngine
- Module 3 Summary
- Introduction
- Create and throw an exception — throw and new
- Throw an exception — Demo CalcEngine
- Custom Exceptions
- Declare a custom exception — Demo InvalidStatementException
- Throwing a custom exception — Demo with throws clause
- Chained Exceptions
- Chaining exceptions — Demo CalcEngine
- Access a chained exception — Demo getCause
- Module 4 Summary
- Introduction
- Exception processing organization
- Reorganization to maintain application flow — Demo CalcEngine
- What to do with exception information
- Show information for user and developer — Demo
- Good and bad practices — Do’s and Don’ts
- Summary of module 5 and conclusion of the course
1. Course Overview
Java continues to be one of the most sought-after programming languages. It is used to develop everything: smartphone applications, websites, server-side processing, big data handling, and the functionality of connected objects (Internet of Things).
No matter what application one is building, what is critical to success is the stability of the application. If an app crashes periodically or is unreliable in any way, users will simply stop using it. This is where exceptions come in. Intelligent and effective use of exceptions is the foundation of Java application stability.
Prerequisites
Before starting this course, it is recommended that you are already familiar with:
- Basics of Java Programming Language
- Java classes
Topics covered in this course
- The important role of exceptions in application stability
- Using try/catch blocks to handle errors
- Improved resource management with try-with-resources
- Handling checked (checked) versus unchecked (unchecked) exceptions
- Creating custom exceptions
- Developing an exception handling strategy
2. Handling Exceptions
2.1 Introduction
In this module, we discuss exception handling and how to start working with them. The topics covered in this module are:
- The role of exceptions — Why do exceptions exist?
- Try/catch blocks — How to handle exceptions in code
- The finally block — Incorporate cleanup into try/catch blocks
- Automating cleanup — Simplify code by automating cleanup
Development environment used
The course was created with a specific version of Java 17 and IntelliJ IDEA. The course information also applies to neighboring versions of these tools.
2.2 The role of exceptions
As an application developer, a reality we face is that programs will encounter errors, and our applications need to be notified of these errors and must handle them.
History of error handling mechanisms
Over the years, several mechanisms have been tried to make working with errors easier:
- Error codes — Methods often returned an error code that had to be constantly checked explicitly in the code.
- Global error codes and thread-level error codes — Although they provided the necessary information, dealing with the errors themselves was often very laborious.
What we need: a structured mechanism
What’s really needed is a mechanism to handle errors effectively — something that signals that something has gone wrong, without requiring constant checks for error codes or error flags.
This is exactly what exceptions provide. An exception is a language mechanism that allows you to:
- Report that an error has occurred
- Provide a structured way to handle this error
The beauty of exceptions is that they eliminate the need to check error codes or look at error flags. Instead, they signal that an error has occurred and we have a structured mechanism to handle these errors.
2.3 Try/catch blocks
The way to handle errors in Java is to rely on a try/catch block.
Structure of a try/catch block
try {
// Le code qu'on essaie d'exécuter (le travail normal)
} catch (Exception ex) {
// Le code pour gérer l'erreur si une exception se produit
}
Operation
- The try block contains the normal code — the work we are trying to do. As long as everything goes well, this block runs to completion, and nothing special has to happen.
- In case of an exception, the code immediately exits the try block. No other code in the try block will execute.
- Control is transferred to the catch block. This block contains error handling code — the code we want to run in the event of a problem.
- The catch block receives information about the exception — the variable (here
ex) whose type isExceptionreceives information about the exception.
Important: The catch block only executes if a corresponding exception is thrown. As long as everything goes well in the try block, the code in the catch block never executes.
Simple example
int i = 10;
int j = 2;
try {
int result = i / (j - 2); // Division par 0 possible
System.out.println(result);
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
In this example, if j is 2, j - 2 is 0 and an ArithmeticException will be thrown. Without the try/catch block, the application would crash. With the try/catch block, the exception is caught and the code in the catch is executed.
Advantages of try/catch
- We have a distinct area of code which contains the work we are trying to do (the try block).
- We have a separate zone distinctly responsible for handling the error (the catch block).
- Work and error handling are coupled together in the overall application flow.
- Whether the job succeeds or an exception occurs, all code after the try/catch block continues normally.
- This allows us to protect our application code in a structured and meaningful way.
2.4 Managing cleanup with finally
When running work in our applications, it is often necessary to perform cleanup. This often manifests itself when working with external resources:
- If we read from a file → we must close this file
- If we work with a database → we want to close the connection to the database
- If we communicate on a network → we want to close the network connection
The problem with try/catch alone
When working with try/catch blocks, you may need to perform cleanup not only when the job succeeds, but also in case of an exception. We need an easy way to ensure that cleanup occurs whether the try block runs to completion or an exception occurs.
The finally block
This is where the finally block comes in. The finally block can be added to a try/catch block. The code placed in the finally block executes in all cases:
- If the try block runs to completion → after the try block, control is transferred to the finally block to perform cleanup.
- If an exception occurs in the try block and control is transferred to the catch block → when the catch block finishes, the finally block also has a chance to execute.
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(args[0]));
// Travail avec le fichier
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
} finally {
// Le nettoyage s'exécute dans TOUS les cas
if (reader != null) {
try {
reader.close();
} catch (Exception ex) {
System.out.println("Error closing: " + ex.getMessage());
}
}
}
The three distinct areas of work
With the try/catch/finally mechanism, we have:
- The try → the work we try to do
- The catch → error management
- The finally → cleaning
2.5 Try/catch/finally implementation — Demo CalcEngine (before)
CalcEngine application overview
The demo application used throughout the course is CalcEngine. It’s a small application that allows you to specify a mathematical operation and two numerical values. These numeric values can be either literal numbers or words representing these numbers. When the code runs, it performs this calculation and displays the results.
Project structure:
CalcEngine/
├── src/
│ └── com/pluralsight/calcengine/
│ ├── Main.java
│ └── MathOperation.java
└── democalculations.txt
democalculations.txt file (before module 2)
add 4 four
subtract eight two
multiply seven 6
divide 6 0
MathOperation.java — Enum of operations
package com.pluralsight.calcengine;
public enum MathOperation {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}
Main.java — Version before (manual with finally)
The before version of the application does not yet use try-with-resources. It handles the finally manually:
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileReader;
public class Main {
public static void main(String[] args) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(args[0]));
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
// NOTE: la fermeture manuelle du reader dans finally
// serait ajoutée ici (voir section suivante)
}
private static void performOperation(String inputLine) {
String[] parts = inputLine.split(" ");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
}
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
result = leftVal / rightVal;
break;
}
return result;
}
static int valueFromWord(String word) {
String[] numberWords = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
int value = -1;
for (int index = 0; index < numberWords.length; index++) {
if (word.equals(numberWords[index])) {
value = index;
break;
}
}
if (value == -1)
value = Integer.parseInt(word);
return value;
}
}
Problems with manual cleanup in finally
Incorporating appropriate cleanup code into try/catch blocks is important, but doing the cleanup manually can be quite laborious in some cases, because you often have to:
- Include null checks to ensure that the resource you are trying to clean up was created.
- Put a try/catch in the finally block itself because the cleanup job can also throw an exception.
These things are very common. This need for null checks and exception handling in our cleanup comes up again and again. It would be nice to be able to automate the cleaning process a little.
2.6 Automating cleanup — AutoCloseable and try-with-resources
Java provides a mechanism to automate cleanup. There are two parts to cleaning automation.
Part 1 — AutoCloseable Interface
For a type to support automated cleanup, it must implement the AutoCloseable interface. The role of this interface is simple: indicate that the type supports automatic cleaning.
The AutoCloseable interface has only one method: the close() method. Any guy that implements this interface is responsible for putting all necessary cleanup code into its close() method. Calling close() on this type ensures that the resource is properly cleaned up.
The Closeable interface
Most types do not directly implement AutoCloseable. They usually implement other interfaces that inherit from AutoCloseable. The most common is the Closeable interface:
- It inherits from
AutoCloseable - It doesn’t even add other methods (just the
close()method)
So when working with a type that inherits from Closeable or any other interface that inherits from AutoCloseable, that type supports automatic cleanup.
Part 2 — try-with-resources
To take advantage of automatic cleanup in our code, we use the try-with-resources syntax. The resource declaration is made in parentheses just after the try keyword:
try (TypeDeResource nomVariable = new TypeDeResource(...)) {
// travail avec la ressource
} catch (Exception ex) {
// gestion des erreurs
}
With try-with-resources:
- Java supports all resource closure details.
- Java checks that the resource is not null before trying to call
close()on it. - The single catch block handles both exceptions that occur in the try block AND exceptions that may occur when closing the resource.
We therefore no longer need:
- Declare resource outside try
- Write an explicit finally block
- Check for null in finally
- Nest a try/catch in finally
2.7 Transition to automatic cleaning — Demo CalcEngine (after)
Main.java — Version after (with try-with-resources)
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileReader;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
}
private static void performOperation(String inputLine) {
String[] parts = inputLine.split(" ");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
}
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
result = leftVal / rightVal;
break;
}
return result;
}
static int valueFromWord(String word) {
String[] numberWords = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
int value = -1;
for (int index = 0; index < numberWords.length; index++) {
if (word.equals(numberWords[index])) {
value = index;
break;
}
}
if (value == -1)
value = Integer.parseInt(word);
return value;
}
}
Key points of converting to try-with-resources
- The
BufferedReaderis declared and instantiated in the parentheses of the try, which binds it to the try block. - The manual finally block is removed completely — Java handles everything automatically.
- The single catch block now handles both try block exceptions AND those that may occur when closing the reader.
2.8 Module 2 Summary
Key takeaways from this module:
| Concept | Description |
|---|---|
| Exceptions | Used to report errors; eliminate the need for error codes or error flags. |
| Try block | Contains the normal code, the work we are trying to do. |
| Catch block | Contains error handling code. Runs only if a matching exception is thrown. |
| finally block | Contains the cleanup code. Runs in all cases (success or exception). |
| AutoCloseable | Interface indicating that a type supports automatic cleanup via the close() method. |
| Closeable | Interface inheriting from AutoCloseable — most common for types using resources. |
| try-with-resources | Syntax that automates cleanup: try (Resource r = new Resource(...)) {...}. Java handles closure, null checks and closure exceptions. |
To use try-with-resources, a type must indicate that it supports this feature by implementing the AutoCloseable interface or one of the interfaces that inherits from it (such as Closeable).
3. Understanding Exception Types
3.1 Introduction
This module examines how the type system represents exceptions. Exceptions are actually just represented by classes, which allows them to be handled by type, allowing different errors to be handled differently.
Topics covered:
- How exceptions are represented by classes and organized in hierarchy
- checked exceptions and unchecked exceptions — understanding the difference helps write exception handling more effectively
- Exceptions and methods — exceptions can cross method boundaries
3.2 The hierarchy of exception classes
All classes representing exceptions have a common base class: the Exception class.
Class hierarchy
Object
└── Throwable
├── Error
│ └── (erreurs critiques JVM, ex: OutOfMemoryError)
└── Exception
├── RuntimeException (exceptions non vérifiées — unchecked)
│ ├── ArithmeticException (division par 0)
│ ├── NullPointerException (utilisation d'une référence null)
│ ├── ArrayIndexOutOfBoundsException
│ ├── IllegalArgumentException
│ └── ...
├── IOException (exceptions vérifiées — checked)
│ └── FileNotFoundException
└── ... (autres exceptions vérifiées)
Key points of the hierarchy
- The
Exceptionclass descends from theObjectclass, but not directly — there is an intermediate class: theThrowableclass. It is theThrowableclass which provides the capabilities allowing the language to throw exceptions. - There are other classes that inherit from
Throwable, notably theErrorclass. Errors generally represent serious problems on the JVM side from which we generally cannot recover. - The
Exceptionclass is a direct descendant ofThrowableand provides the fundamental ability to be an exception.
Different levels of granularity
Some exception classes represent broad categories of errors (eg: Exception, IOException). Other classes represent very specific types of errors (eg: FileNotFoundException, ArithmeticException). Not all errors necessarily have their own individual exception class. This is a good thing because it allows you to decide what level of error handling granularity you want to put in your code.
3.3 Exception handling by type — Multiple catch blocks
As exceptions are represented by classes, this allows you to be more specific in exception handling. A try block can have multiple catch blocks.
Multiple catches
Each catch block indicates what type of exception it wants to handle by the type of the exception class it catches:
try {
// Travail potentiellement risqué
} catch (ArithmeticException ex) {
// Gestion spécifique de la division par 0
System.out.println("Arithmetic error: " + ex.getMessage());
} catch (Exception ex) {
// Gestion générale de toutes les autres exceptions
System.out.println("General error: " + ex.getMessage());
}
Order of catch blocks — Important rule
When a try contains several catches, Java tests these catches from top to bottom. The first assignable catch is the one that will be used. By “assignable” we mean the first catch that catches either the class representing the specific exception thrown, or a base class of that exception.
Crucial rule: Put most specific exceptions first** and less specific exceptions lower in the list.
Example — Incorrect order (will not compile or work as expected):
// MAUVAIS ORDRE — Exception est trop large, attrape tout avant ArithmeticException
catch (Exception ex) { ... }
catch (ArithmeticException ex) { ... } // Code mort (dead code) !
Example — Correct order:
// BON ORDRE — Du plus spécifique au moins spécifique
catch (ArithmeticException ex) { ... } // Plus spécifique
catch (Exception ex) { ... } // Moins spécifique
Interest in multiple catches
A catch that catches the Exception class itself says: “I want to handle any exception represented directly by the Exception class or any class that inherits from Exception.” Since all classes representing exceptions inherit from Exception, this catch catches any exception of any type thrown.
But having just this single catch that handles the Exception class doesn’t give the chance to handle individual errors in a more specific way.
3.4 Checked and unchecked exceptions (checked vs unchecked)
Exception classes are divided into two main categories:
Checked Exceptions
These are exceptions where the compiler will throw an error if this exception is not handled. In other words, when we write our code and compile it, if our code has the possibility of throwing an exception considered checked, if we do not have a catch block for this exception class or one of its base classes, our code will not compile. The compiler will throw an error.
Examples of checked exceptions:
IOExceptionFileNotFoundException(inherits fromIOException)SQLException
How to recognize a checked exception: These are the exceptions that do NOT inherit from RuntimeException.
Unchecked Exceptions
These are exceptions where the compiler does not force error handling. In the case of unchecked exceptions, if you write code with the possibility of generating an unchecked exception, the compiler does not care about it. The compiler will not throw an error if we do not catch this exception or one of its base classes.
Examples of unchecked exceptions:
ArithmeticException(divide by 0)NullPointerException(using a null reference)ArrayIndexOutOfBoundsExceptionIllegalArgumentException
How to recognize an unchecked exception: These are the exceptions that inherit from RuntimeException.
The ABSOLUTE rule to remember
Whether an exception is checked or unchecked, if it occurs at run time and is not caught, the program will crash.
The only difference between checked and unchecked exceptions is compile time. At runtime, any unhandled exception can crash the application.
3.5 Handling multiple exceptions — Demo CalcEngine
Main.java — Module 3 (after) with multiple catches for checked exceptions
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
processFile(reader);
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
private static void processFile(BufferedReader reader) throws IOException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
}
// ... autres méthodes (voir section 2.7)
}
Why FileNotFoundException before IOException?
FileNotFoundException inherits from IOException. If we put IOException first, the IOException catch would also catch FileNotFoundException and the FileNotFoundException catch would never execute (dead code).
Points observed in the demo
Starting with a single catch that handles Exception and removing it, the IntelliJ IntelliJ IDE underlines the code in red, indicating errors:
- Creating the
FileReader→Unhandled exception: FileNotFoundException(checked → compiler requires it) - The
reader.readLine()call →Unhandled exception: IOException(checked → compiler requires it)
As FileNotFoundException inherits from IOException, we could only put a catch for IOException. But by adding a specific catch for FileNotFoundException first (since it is the most specific), we can provide a more precise error message (show the name of the file not found).
3.6 Handling unchecked exceptions — Demo CalcEngine
Issue revealed at runtime
Even with the catches for FileNotFoundException and IOException in place, the code compiles. But by configuring the project to point to the democalculations.txt file (which contains divide 6 0), the program crashes with an ArithmeticException (divide by 0).
This problem occurs because:
ArithmeticExceptionis an unchecked exception (inherits fromRuntimeException)- The compiler did not require it to be handled
- But the lack of management still caused a crash at runtime
Solution — Add a catch for Exception
We cannot always know in advance all possible unchecked exceptions. The best approach is to add a catch for the Exception class as a safety net:
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (Exception ex) {
// Filet de sécurité pour toutes les autres exceptions (y compris unchecked)
System.out.println("Error processing file - " + ex.getMessage());
}
Thus, the application robustly handles exceptions:
- Catches for known specific checked exceptions
- A catch for
Exceptionto handle any other exceptions that may occur
3.7 Exceptions and methods — Traversal of the call stack
An exception thrown in a method can actually cross method boundaries. If an exception is thrown in a method and that method does not handle the exception, the exception will actually travel up the call stack.
Visualization of the exception flow through the call stack
methodA() { ← L'exception est attrapée ici par le catch de methodA
try {
methodB(); ← Appelle methodB dans le bloc try
} catch (Exception ex) { ... }
}
methodB() { ← Alloue de l'espace sur la call stack
// ...travail...
methodC(); ← Appelle methodC
}
methodC() { ← Alloue de l'espace sur la call stack
// ...travail...
throw new Exception(); // ← L'exception est lancée ici
// methodC ne gère pas l'exception
}
Flow:
methodCthrows an exception and does not handle it- The exception starts moving up the call stack
- It arrives in
methodBwhich does not handle it either - It arrives in
methodAwhich has a catch → the exception is caught methodCandmethodBare both unwound completely
Important implications
Because exceptions can move up the call stack, this allows an exception thrown in one method to be caught by a different method that called the method that threw the exception. This has important implications for how to organize exception handling.
Method responsibilities with checked exceptions
If a method throws a checked exception and does not handle it, the method is responsible for documenting that this exception can occur. It does this using the throws` clause:
private static void processFile(BufferedReader reader) throws IOException {
// ... code pouvant lancer IOException
}
A method must list in its throws clause all checked exceptions which may occur and which the method does not handle itself.
3.8 Handling exceptions between methods — Demo CalcEngine
Refactoring — Extract logic into a processFile method
In the demo, the code for reading and processing the file is extracted into a separate processFile method. This method receives a BufferedReader as a parameter.
private static void processFile(BufferedReader reader) throws IOException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
}
Why the throws clause is necessary
In processFile, the call to reader.readLine() is underlined in red in the IDE because readLine() can throw an IOException — a checked exception. processFile has two choices:
- Put a try/catch in
processFileto handleIOException - Propagate the exception to the calling method by adding
throws IOExceptionto the signature ofprocessFile
In this case, we choose to propagate the exception with the throws IOException clause, because the exception code is already in place in main(). This means that IOException is now part of the contract of the processFile method.
Result
// main() continue à gérer les exceptions centralement
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
processFile(reader); // L'exception de processFile remonte jusqu'ici
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
Our code can easily separate work into the appropriate methods while centralizing exception handling in main().
3.9 Module 3 Summary
| Concept | Description |
|---|---|
| Exceptions represented by classes | Organized in a hierarchy, all inheriting from Exception. |
| Multiple catches | A try can have several catchs, tested from top to bottom. Order: more specific first. |
| Checked exceptions | The compiler throws an error if not handled. Inherit from Exception but NOT from RuntimeException. |
| Unchecked exceptions | The compiler does not require management. Inherit from RuntimeException. |
| Absolute rule | Any unhandled exception at runtime crashes the application, whether checked or unchecked. |
| Exceptions and methods | An exception can cross method boundaries up the call stack. |
| Throws clause | A method that does not handle a checked exception must declare it in its throws clause. |
4. Creating Custom Exceptions
4.1 Introduction
In previous modules, the focus was on handling exceptions thrown by other parts of the code. This module covers the other side: how to throw an exception from our code and how to create our own custom exception types.
Topics covered:
- Throwing an exception from our code — How to indicate that an error has occurred
- Create custom exceptions — Java allows you to define custom exception types
- Exception chaining — An exception can wrap another exception
4.2 Create and throw an exception — throw and new
The throw keyword
If the way to handle an exception is to catch it, the way to indicate that an exception has occurred is to throw it. The throw keyword allows you to throw an exception to indicate that a problem has occurred.
Create an exception instance
Before throwing an exception, you must first create the exception. The way to create an exception is to use the new operator. Since exceptions are classes, we create an instance of the class that represents this exception.
// Créer une instance de l'exception
TypeException exception = new TypeException("Description du problème");
// Lancer l'exception
throw exception;
// Forme condensée (la plus courante)
throw new TypeException("Description du problème");
Information to be provided except
When creating an exception, information about this exception must be provided. Generally, we use the class constructor. At a minimum, we will normally pass a String type value describing the problem that occurred. In some cases there may be additional information to include.
4.3 Throwing an exception — Demo CalcEngine
Problem — Division by 0 without clear message
In the execute method, during a division, if rightVal is 0, this initiates a division by 0. It would be better to provide a more meaningful message.
Solution — Throw an IllegalArgumentException
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
if (rightVal == 0) {
// Technique longue (commentée dans le code final) :
// IllegalArgumentException exception =
// new IllegalArgumentException("Zero rightVal not permitted with divide operation");
// throw exception;
// Technique condensée (forme finale) :
throw new IllegalArgumentException("Zero rightVal not permitted with divide operation");
}
result = leftVal / rightVal;
break;
}
return result;
}
Result
When we perform a division operation with rightVal of 0, instead of getting a simple “ArithmeticException: / by zero”, we now get a much more meaningful message: "Zero rightVal not permitted with divide operation".
The value of being able to throw exceptions from our code is that we can report better information about what happened.
4.4 Custom Exceptions — Custom Exceptions
Java has a rich set of built-in exception types, and in general these meet most needs. But sometimes we need an exception type that is not one of the built-in exception types.
Creating a custom exception class
The way to create a custom exception is to define your own exception class. Important reminder: an exception is a class, but not just any class can be an exception. All classes representing exceptions must inherit from the Exception class (directly or indirectly).
In most cases, when creating a custom exception class, one inherits directly from the Exception class itself. This means that our custom exception is considered a checked exception.
What to add to the custom exception class
Most of the necessary functionality is provided by the Exception base class. The additional work required is minimal. Generally, the only members that should be added are the appropriate constructors.
public class MonException extends Exception {
// Constructeur avec message
public MonException(String message) {
super(message);
}
// Constructeur avec message ET exception cause (pour le chaining)
public MonException(String message, Exception cause) {
super(message, cause);
}
// Si des membres supplémentaires sont nécessaires (getters, etc.),
// on peut les ajouter librement
}
Note: If a custom exception requires additional members (methods, getters, setters, etc.), you are completely free to add them. But generally, the only members to provide are the builders.
4.5 Declare a custom exception — Demo InvalidStatementException
Context — Problem in performOperation
The performOperation method is responsible for taking the input statement and breaking it down into its parts (operation, leftVal, rightVal). If the user doesn’t provide exactly 3 parts, that’s a problem. Ideally, one would use an exception to indicate this problem — and no existing exception would do very well.
Creating the InvalidStatementException class
By convention, the name of any class representing an exception ends with the word Exception.
package com.pluralsight.calcengine;
public class InvalidStatementException extends Exception {
// Constructeur avec message uniquement
public InvalidStatementException(String message) {
super(message);
}
// Constructeur avec message ET exception cause (pour le chaining)
public InvalidStatementException(String message, Exception ex) {
super(message, ex);
}
}
Creation steps in the IDE:
- In the Project window, right-click on the package name
- New → Java Class
- Name:
InvalidStatementException - The class is created as a normal class
- Add
extends Exceptionto make it a real exception class - Add appropriate constructors
4.6 Throwing a custom exception — Demo with throws clause
Using InvalidStatementException in performOperation
private static void performOperation(String inputLine) throws InvalidStatementException {
String[] parts = inputLine.split(" ");
if (parts.length != 3)
throw new InvalidStatementException(
"Statement must have 3 parts: operation leftVal rightVal");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
}
Throw propagation — Cascading effect
Because InvalidStatementException is a checked exception, any method that does not handle it must declare it in its throws clause. In cascade:
performOperationthrowsInvalidStatementException→ should declarethrows InvalidStatementExceptionprocessFilecallsperformOperation→processFilemust also declarethrows IOException, InvalidStatementExceptionmain()callsprocessFile→main()must handleInvalidStatementExceptionwith a catch
// Dans main() — ajout d'un catch pour InvalidStatementException
} catch (InvalidStatementException ex) {
System.out.println("Error invalid statement - " + ex.getMessage());
if (ex.getCause() != null)
System.out.println(" caused by " + ex.getCause());
}
4.7 Chained Exceptions
Exception chaining allows you to create an exception that wraps another exception.
Why chain exceptions?
It may happen that the underlying error occurs and throws an exception, but we do not want to directly throw that underlying exception. We want to throw a more meaningful exception, perhaps something that better represents the work at the application level. But we don’t want to lose the underlying exception that represents the error that actually occurred.
A chain of exceptions allows throwing this more significant exception while preserving the information of the original exception.
How to chain exceptions — Two approaches
Approach 1 — initCause method (less common):
Exception nouvelleException = new Exception("Description de haut niveau");
nouvelleException.initCause(exceptionOriginale);
throw nouvelleException;
Approach 2 — Manufacturer (most common):
throw new Exception("Description de haut niveau", exceptionOriginale);
Most exception types include such a constructor accepting an exception as a second parameter. When creating custom exceptions, it is recommended to include this type of constructor as well.
Access chained exception — getCause
The Exception class provides a method getCause() which returns a reference to the exception which is wrapped by the current exception:
catch (Exception ex) {
System.out.println("Erreur : " + ex.getMessage());
if (ex.getCause() != null) {
System.out.println("Causé par : " + ex.getCause());
}
}
4.8 Chaining exceptions — Demo CalcEngine
Improved InvalidStatementException for chaining
The second constructor of InvalidStatementException already supports chaining:
public class InvalidStatementException extends Exception {
public InvalidStatementException(String message) {
super(message);
}
// Ce constructeur accepte l'exception cause
public InvalidStatementException(String message, Exception ex) {
super(message, ex);
}
}
Using chaining in performOperation
In performOperation, certain conversions may throw exceptions (eg: conversion of an invalid operator via MathOperation.valueOf(), conversion of a non-numeric value via Integer.parseInt()). We can wrap these exceptions in an InvalidStatementException to provide more context:
private static void performOperation(String inputLine) throws InvalidStatementException {
try {
String[] parts = inputLine.split(" ");
if (parts.length != 3)
throw new InvalidStatementException(
"Statement must have 3 parts: operation leftVal rightVal");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
} catch (InvalidStatementException ex) {
throw ex; // Re-lancer l'exception déjà de bon type
} catch (Exception ex) {
// Envelopper toute autre exception dans InvalidStatementException
throw new InvalidStatementException("Error processing statement", ex);
}
}
Logic explanation:
- We wrap the entire body of the method in a try block
- We first catch
InvalidStatementExceptionand rethrow it as is (so as not to wrap it in itself) - We catch
Exceptionand wrap it in anInvalidStatementExceptionwith the context message AND the original exception in question
4.9 Accessing a chained exception — Demo getCause
In main() — Get to the cause
} catch (InvalidStatementException ex) {
System.out.println("Error invalid statement - " + ex.getMessage());
if (ex.getCause() != null)
System.out.println(" caused by " + ex.getCause());
}
Test with invalid value
In democalculations.txt, if we put a line like divide 60 1o (with the letter “o” instead of “0”), when trying to convert “1o” to a number via Integer.parseInt("1o"), an exception will be thrown. This exception will be wrapped in an InvalidStatementException.
Console output:
add 4 four = 8
subtract eight two = 6
multiply seven 6 = 42
Error invalid statement - Error processing statement
caused by java.lang.NumberFormatException: For input string: "1o"
We thus see two levels of information:
- The
InvalidStatementExceptionmessage (high-level context) - The cause of type
NumberFormatException(detailed underlying exception)
4.10 Module 4 Summary
| Concept | Description |
|---|---|
| throw | Keyword to throw an exception from our code. |
| new | Operator used to create an instance of the exception before throwing it. |
| Exception creation | throw new TypeException("description"); |
| Custom exception | Class that inherits from Exception (directly or indirectly). If directly inherits from Exception → checked exception. |
| Members of a custom exception | Generally only the appropriate constructors are needed. |
| Throws clause | Any method that does not handle a checked exception must declare it in its throws clause. |
| Chaining exception | One exception can wrap another. Allows you to throw a high-level exception while preserving the original exception. |
| initCause | Method to chain an exception after its creation. |
| Constructor(message, cause) | Most common way to chain — pass the original exception to the constructor. |
| getCause() | Method to access the wrapped exception. Returns null if no cause. |
5. Developing an Exception Handling Strategy
5.1 Introduction
This module examines considerations to make when working with exceptions in code. There are several things to think about in how to use exceptions, and by looking at them you can get a much more efficient use of the exception handling mechanism.
Topics covered:
- Organization of exception handling — Where do we place catch blocks in the code?
- What to do with exception information — The answer depends on who you report this information to.
- Do’s and Don’ts — Exceptions are an incredibly powerful mechanism, and because of their capabilities, they are often misused.
5.2 Organization of exception processing
When it comes to organizing exception handling, there are two general ways to go about it: centralized management and localized management.
Centralized Exception Handling
Centralized exception handling generally means grouping exception handling. High up in the application logic, we can have a try block with a long series of catches that will handle all the different exceptions that may arise during code execution.
Advantages:
- Tends to simplify exception handling because all handling code tends to be in one place (or a small number of places).
- Creating this code and working with it tends to be easy because it’s all put together.
Disadvantages:
- May make recovering an exception more difficult. Let’s remember how exceptions work: when an exception is thrown, it begins to unwind the call stack and goes further and further into the code. As a result, the exception may cause the processing logic to exit. If one has left the processing logic, continuing to work after the error can be difficult, even if there was a way to recover and continue.
Localized Exception Handling
Localized handling means placing exception handling closer to the point in the code where the exception might occur.
Advantages:
- Make it easier to recover from an exception. If we put exception handling directly in the processing loop, we can handle the error and then continue to do the work in that loop if possible.
Disadvantages:
- May make management more laborious. If you have a bunch of try/catch blocks spread throughout the code, this can be a bit of a pain to manage.
Real Practice — A Mixture of Both
In practice, we generally don’t use one or the other exclusively — we use a mixture of both at different levels. The key is to balance these tradeoffs, having the appropriate exception handling in the appropriate parts of the code.
5.3 Reorganization to maintain application flow — Demo CalcEngine
Problem — Centralizing InvalidStatementException in main() cuts processing
With InvalidStatementException caught in main(), a single invalid statement stops all processing. Subsequent valid calculations are never performed.
Explanation: If the catch for InvalidStatementException is in main(), as soon as an InvalidStatementException is thrown somewhere in processFile, the exception goes back to main(), which completely escapes the try-with-resources block and the read loop. All subsequent statements in the file are ignored.
democalculations.txt (module 5)
add 4 four
subtract eight two extra ← ligne invalide (4 parties au lieu de 3)
multiply seven 6
divide 60 10
With InvalidStatementException handled in main(), only add 4 four = 8 is displayed, then the error is reported and the remaining 2 valid calculations never run.
Solution — Move catch to processFile
By moving the InvalidStatementException catch into processFile, directly in the processing loop, we can handle each error individually and continue processing:
private static void processFile(BufferedReader reader) throws IOException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
try {
performOperation(inputLine);
} catch (InvalidStatementException ex) {
// Gestion de l'erreur pour CET énoncé spécifique
System.out.println(ex.getMessage() + " - " + inputLine);
writeInvalidStatementExceptionToLog(ex, inputLine);
// La boucle CONTINUE — les énoncés suivants sont traités
}
}
Result with localized management
add 4 four = 8
Statement must have 3 parts: operation leftVal rightVal - subtract eight two extra
multiply seven 6 = 42
divide 60 10 = 6
The application continues to run even after an error, processing all valid statements.
Exceptions that remain centralized in main()
Certain exceptions are always managed centrally in main() because they are linked to global problems (impossible to continue processing in this case):
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
5.4 What to do with exception information
Exceptions have real power: they carry information about exactly what went wrong. With all this information in the exception, what do we do once we catch it?
The two exception audiences
An important thing to keep in mind: exceptions actually have two different audiences.
Audience 1 — The end user (end user) This is the person at the computer when the error occurs. She will usually be the first to know that something has gone wrong. Reporting to the end user:
- The basic purpose is simply to inform him that something went wrong
- The information provided should be clear and simple
- Say something happened and give a general description of what happened
- If user is running data from a large dataset, tell them where the problem occurred
- Do not overload the user: he does not need a detailed stack trace
Audience 2 — The developers and support team These are the people responsible for solving problems. Reporting to developers:
- Information should be extremely detailed
- Include things like call stack (stack trace)
- Include everything they might need to understand the problem
- This audience was probably not present when the exception actually occurred
- It is therefore necessary to persist the information → write in a logging system (logging system)
5.5 Show information for user and developer — Demo
In main() — Before refactoring (too much user info)
catch (InvalidStatementException ex) {
// PROBLÈME : affiche les détails de l'exception chaînée à l'utilisateur
System.out.println("Error invalid statement - " + ex.getMessage());
if (ex.getCause() != null)
System.out.println(" caused by " + ex.getCause()); // Trop de détail pour l'utilisateur
}
In processFile — Final version (after refactoring)
private static void processFile(BufferedReader reader) throws IOException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
try {
performOperation(inputLine);
} catch (InvalidStatementException ex) {
// Pour l'utilisateur final : message simple + ligne problématique
System.out.println(ex.getMessage() + " - " + inputLine);
// Pour les développeurs : journalisation détaillée
writeInvalidStatementExceptionToLog(ex, inputLine);
}
}
writeInvalidStatementExceptionToLog method
static void writeInvalidStatementExceptionToLog(InvalidStatementException ex, String inputLine) {
System.err.println("");
System.err.println("*********************************");
System.err.println("Information written to log system");
System.err.println("*********************************");
System.err.println(ex.getMessage() + " - " + inputLine);
if (ex.getCause() != null)
System.err.println(" caused by " + ex.getCause());
ex.printStackTrace(System.err); // Stack trace complète pour les développeurs
}
Note: In this demo example,
System.erris used to simulate writing to a logging system. In a real application, one would use a logging framework like Log4j, SLF4J, or the java.util.logging built into Java.
Distinction of the information provided
| Hearing | Information provided | Destination |
|---|---|---|
| End user | Clear message + problematic line | System.out (screen) |
| Developers / Support | Message + cause + complete stack trace | System.err → Logging system |
5.6 Good and bad practices — Do’s and Don’ts
Exceptions have a lot of powerful capabilities, and because of these capabilities they can be used very effectively for error handling. But also because of these capabilities, exceptions are often used inappropriately.
What we MUST do (Do’s)
✅ Use exceptions to indicate errors or other exceptional circumstances Exceptions are designed for exceptional circumstances — when something has gone wrong or when something extremely unusual has happened.
✅ Include data specifically related to the error in exceptions The power of exceptions is that they can include data. This data must be information specifically related to the error or exceptional circumstance. We want to be very focused: an exception occurs because something went wrong, and we include information about what went wrong.
What NOT to do (Don’ts)
❌ Does NOT use exceptions for standard application control flow or conditional logic One of the real powers of an exception is that it can be thrown very low in the call stack, run through a series of method calls, and branch directly into a catch block. And it’s fantastic for error handling. But we should never have standard application logic that relies on an exception to do conditional control flow.
We never want to use exceptions as a shortcut to exit a series of methods and branch into another part of the logic. That’s not what exceptions are for. This is very inefficient and, more importantly, it makes applications very difficult to understand and debug.
❌ Does NOT use exceptions to carry standard data in place of parameters or return values Data in exceptions is for exceptional circumstances. Exceptions should not be used as a means of transporting data instead of passing information through parameters or providing return values from methods. Again, this makes the code very difficult to follow and extremely difficult to debug.
The ImproperExceptionUse project — Example of bad practices
In the before folder of the Module 5 exercises, a project named ImproperExceptionUse shows how NOT to use exceptions. This is a version of the CalcEngine application that misuses exceptions to accomplish its job.
Examples of bad practices in this project:
-
Exceptions used to control logic flow (instead of traditional conditional blocks) — Each math operation has its own exception class (
ProcessAddException,ProcessSubtractException, etc.), and an exception is thrown to indicate which operation to perform, then caught to execute it. -
Exceptions used to unwind methods abnormally — Instead of returning normally from methods, an exception is thrown as a way to jump from one method to another (short-circuiting normal return).
-
Exceptions used to pass standard data — Exceptions carry
leftValandrightValdata which is normal business data, not error information.
ProcessAddException class — Example of misuse:
package com.pluralsight.improperexceptionuse;
/*
Cette exception est utilisée de façon INCORRECTE dans le code de l'application
car la classe d'exception n'est pas utilisée pour indiquer une erreur ou
un autre scénario exceptionnel.
Au contraire, cette exception est utilisée pour indiquer quand une OPÉRATION
D'ADDITION doit être effectuée (une partie valide et non exceptionnelle
du comportement standard de l'application).
Cette exception est utilisée pour effectuer un flux de contrôle standard
dans le comportement normal de l'application. CE N'EST PAS LA BONNE FAÇON
d'utiliser les exceptions.
*/
public class ProcessAddException extends Exception {
private int leftVal;
private int rightVal;
public ProcessAddException(int leftVal, int rightVal) {
this.leftVal = leftVal;
this.rightVal = rightVal;
}
public int getLeftVal() { return leftVal; }
public int getRightVal() { return rightVal; }
}
Main.java of ImproperExceptionUse (excerpt) — The wrong way:
public static void main(String[] args) {
int result = 0;
boolean done = false;
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
while (!done) {
try {
// Chaque ligne est traitée par processFile
// Le travail dans processFile appelle une autre méthode qui lance
// une exception SPÉCIFIQUE pour indiquer quelle opération effectuer.
// Le code branche ensuite vers l'opération en GÉRANT l'exception.
// C'est une TRÈS MAUVAISE technique.
processFile(reader);
done = true;
} catch (ProcessAddException ex) {
result = doAdd(ex); // Faire l'addition via le catch !
} catch (ProcessSubtractException ex) {
result = doSubtract(ex); // Faire la soustraction via le catch !
} catch (ProcessMultiplyException ex) {
result = doMultiply(ex);
} catch (ProcessDivideException ex) {
result = doDivide(ex);
}
if (!done)
System.out.println(result);
}
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error reading file - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
This code is provided as a demonstration for educational purposes — to show what to avoid. These techniques, although seen in real applications created by other developers, should not be used.
5.7 Summary of module 5 and conclusion of the course
Module 5 Key Points
Organization of exception management:
- Balancing the tradeoffs between centralized and localized management
- Centralized management → simplifies the management code but makes recovery more difficult
- Localized management → facilitates recovery but can be more laborious
- In practice: use a mixture of both at different appropriate levels
Exception information — Two audiences:
- End user: simple, clear information, with indication of the location of the problem
- Developers/Support: extremely detailed information (stack trace, cause, context) → persisted in a logging system
Best practices:
- Use exceptions for errors and exceptional circumstances
- Data in exceptions = error specific information
Bad practices to avoid:
- NEVER use exceptions for standard control flow
- NEVER use exceptions to transport standard data
- NEVER use exceptions as a shortcut for unrolling methods
Course Conclusion
Exceptions are a rich and powerful mechanism for writing truly stable and robust applications with well-designed error handling. By using them correctly, it greatly improves the quality of the applications created.
6. Demo Project: CalcEngine — Overview
The CalcEngine project is the demo application used throughout the course. It is a simple Java application that reads mathematical operations from a text file and executes them.
General structure
CalcEngine/
├── src/
│ └── com/pluralsight/calcengine/
│ ├── Main.java ← Classe principale
│ ├── MathOperation.java ← Enum des opérations
│ └── InvalidStatementException.java ← Exception personnalisée (modules 4-5)
└── democalculations.txt ← Fichier d'entrée
Evolution of the project through the modules
| Module | Main changes |
|---|---|
| 02 — before | Manual management of the reader (without try-with-resources), a single catch Exception |
| 02 — after | Conversion to try-with-resources |
| 03 — after | Added multiple catches (FileNotFoundException, IOException, Exception), processFile method extracted with throws clause |
| 04 — after | Added InvalidStatementException, checking number of parts, cascading throws, exception chaining |
| 05 — after | Moved InvalidStatementException catch to processFile for localized management, added writeInvalidStatementExceptionToLog |
Format of the democalculations.txt file
Each line contains one operation and two values, separated by spaces:
[operation] [valeur1] [valeur2]
Values can be either integers (4, 6) or words representing numbers (four, six, zero…).
Supported operations:
add→ Additionsubtract→ Subtractionmultiply→ Multiplicationdivide→ Division
7. Source Code Files
Module 02 — after: Main.java
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileReader;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
}
private static void performOperation(String inputLine) {
String[] parts = inputLine.split(" ");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
}
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
result = leftVal / rightVal;
break;
}
return result;
}
static int valueFromWord(String word) {
String[] numberWords = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
int value = -1;
for (int index = 0; index < numberWords.length; index++) {
if (word.equals(numberWords[index])) {
value = index;
break;
}
}
if (value == -1)
value = Integer.parseInt(word);
return value;
}
}
Module 02 — MathOperation.java
package com.pluralsight.calcengine;
public enum MathOperation {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}
Module 03 — after: Main.java (multiple catches, processFile method)
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
processFile(reader);
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
private static void processFile(BufferedReader reader) throws IOException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
}
private static void performOperation(String inputLine) {
String[] parts = inputLine.split(" ");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
}
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
result = leftVal / rightVal;
break;
}
return result;
}
static int valueFromWord(String word) {
String[] numberWords = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
int value = -1;
for (int index = 0; index < numberWords.length; index++) {
if (word.equals(numberWords[index])) {
value = index;
break;
}
}
if (value == -1)
value = Integer.parseInt(word);
return value;
}
}
Module 04 — after: InvalidStatementException.java
package com.pluralsight.calcengine;
public class InvalidStatementException extends Exception {
public InvalidStatementException(String message) {
super(message);
}
public InvalidStatementException(String message, Exception ex) {
super(message, ex);
}
}
Module 04 — after: Main.java (custom exception, chaining, cascading throws)
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
processFile(reader);
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (InvalidStatementException ex) {
System.out.println("Error invalid statement - " + ex.getMessage());
if (ex.getCause() != null)
System.out.println(" caused by " + ex.getCause());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
private static void processFile(BufferedReader reader) throws IOException, InvalidStatementException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
performOperation(inputLine);
}
private static void performOperation(String inputLine) throws InvalidStatementException {
try {
String[] parts = inputLine.split(" ");
if (parts.length != 3)
throw new InvalidStatementException(
"Statement must have 3 parts: operation leftVal rightVal");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
} catch (InvalidStatementException ex) {
throw ex;
} catch (Exception ex) {
throw new InvalidStatementException("Error processing statement", ex);
}
}
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
if (rightVal == 0) {
throw new IllegalArgumentException(
"Zero rightVal not permitted with divide operation");
}
result = leftVal / rightVal;
break;
}
return result;
}
static int valueFromWord(String word) {
String[] numberWords = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
int value = -1;
for (int index = 0; index < numberWords.length; index++) {
if (word.equals(numberWords[index])) {
value = index;
break;
}
}
if (value == -1)
value = Integer.parseInt(word);
return value;
}
}
Module 05 — after: Main.java (localized management, logging)
package com.pluralsight.calcengine;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
processFile(reader);
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
private static void processFile(BufferedReader reader) throws IOException {
String inputLine = null;
while ((inputLine = reader.readLine()) != null)
try {
performOperation(inputLine);
} catch (InvalidStatementException ex) {
// Pour l'utilisateur final : message clair + ligne problématique
System.out.println(ex.getMessage() + " - " + inputLine);
// Pour les développeurs : informations détaillées dans le log
writeInvalidStatementExceptionToLog(ex, inputLine);
}
}
private static void performOperation(String inputLine) throws InvalidStatementException {
try {
String[] parts = inputLine.split(" ");
if (parts.length != 3)
throw new InvalidStatementException(
"Statement must have 3 parts: operation leftVal rightVal");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
int result = execute(operation, leftVal, rightVal);
System.out.println(inputLine + " = " + result);
} catch (InvalidStatementException ex) {
throw ex;
} catch (Exception ex) {
throw new InvalidStatementException("Error processing statement", ex);
}
}
static int execute(MathOperation operation, int leftVal, int rightVal) {
int result = 0;
switch (operation) {
case ADD:
result = leftVal + rightVal;
break;
case SUBTRACT:
result = leftVal - rightVal;
break;
case MULTIPLY:
result = leftVal * rightVal;
break;
case DIVIDE:
if (rightVal == 0) {
throw new IllegalArgumentException(
"Zero rightVal not permitted with divide operation");
}
result = leftVal / rightVal;
break;
}
return result;
}
static int valueFromWord(String word) {
String[] numberWords = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
int value = -1;
for (int index = 0; index < numberWords.length; index++) {
if (word.equals(numberWords[index])) {
value = index;
break;
}
}
if (value == -1)
value = Integer.parseInt(word);
return value;
}
static void writeInvalidStatementExceptionToLog(InvalidStatementException ex, String inputLine) {
System.err.println("");
System.err.println("*********************************");
System.err.println("Information written to log system");
System.err.println("*********************************");
System.err.println(ex.getMessage() + " - " + inputLine);
if (ex.getCause() != null)
System.err.println(" caused by " + ex.getCause());
ex.printStackTrace(System.err);
}
}
Module 05 — before (ImproperExceptionUse): Extracted from Main.java
This code illustrates bad practices to avoid.
package com.pluralsight.improperexceptionuse;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
/*
********************************************************************************
* REMEMBER: The code in this project demonstrates a WRONG WAY to use exceptions.
*
* The techniques in this program are provided as a demonstration for educational
* purposes. Although I have seen these techniques used in real applications
* created by other developers, I do not use these techniques and neither should
* you or anyone else.
*
* These code is provided to demonstrate usage scenarios that should be AVOIDED.
*/
public static void main(String[] args) {
int result = 0;
boolean done = false;
try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
while (!done) {
try {
// Chaque ligne du fichier est traitée par processFile
// Le travail dans processFile appelle une autre méthode qui
// lance une exception SPÉCIFIQUE pour indiquer quelle opération
// effectuer. Le code branche ensuite pour faire ce travail en
// GÉRANT l'exception. C'est une TRÈS MAUVAISE technique.
processFile(reader);
done = true;
} catch (ProcessAddException ex) {
result = doAdd(ex);
} catch (ProcessSubtractException ex) {
result = doSubtract(ex);
} catch (ProcessMultiplyException ex) {
result = doMultiply(ex);
} catch (ProcessDivideException ex) {
result = doDivide(ex);
}
if (!done)
System.out.println(result);
}
} catch (FileNotFoundException ex) {
System.out.println("File not found: " + args[0]);
} catch (IOException ex) {
System.out.println("Error reading file - " + ex.getMessage());
} catch (Exception ex) {
System.out.println("Error processing file - " + ex.getMessage());
}
}
private static void processFile(BufferedReader reader)
throws IOException, ProcessAddException, ProcessSubtractException,
ProcessMultiplyException, ProcessDivideException {
String inputLine = null;
int result = 0;
if ((inputLine = reader.readLine()) != null) {
System.out.print(inputLine + " = ");
processInput(inputLine);
}
}
private static void processInput(String inputLine)
throws ProcessAddException, ProcessSubtractException,
ProcessMultiplyException, ProcessDivideException {
String[] parts = inputLine.split(" ");
MathOperation operation = MathOperation.valueOf(parts[0].toUpperCase());
int leftVal = valueFromWord(parts[1]);
int rightVal = valueFromWord(parts[2]);
// L'instruction switch lance une exception SPÉCIFIQUE pour indiquer
// quelle opération effectuer. L'exception sortira de cette méthode
// (processInput), puis causera aussi le déroulement de processFile
// jusqu'à ce que le code arrive au bloc catch dans main.
//
// C'est une MAUVAISE technique car elle va à l'encontre du comportement
// normal des méthodes qui sont appelées puis sortent de façon distincte
// et prévisible.
// ...
}
}
Search Terms
exception · handling · java · se · backend · architecture · full-stack · web · exceptions · calcengine · catch · main.java · custom · finally · invalidstatementexception · try · catches · chaining · checked · class · information · method · points · processfile