Intermediate

Exception Handling in Java SE

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, an...

Training: Exception Handling in Java SE 17

Table of Contents

  1. Course Overview
  2. Handling Exceptions
  1. Understanding Exception Types
  1. Creating Custom Exceptions
  1. Developing an Exception Handling Strategy
  1. Demo Project: CalcEngine — Overview
  2. Source Code Files

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:

  1. The role of exceptions — Why do exceptions exist?
  2. Try/catch blocks — How to handle exceptions in code
  3. The finally block — Incorporate cleanup into try/catch blocks
  4. 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

  1. 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.
  2. In case of an exception, the code immediately exits the try block. No other code in the try block will execute.
  3. 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.
  4. The catch block receives information about the exception — the variable (here ex) whose type is Exception receives 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:

  1. The try → the work we try to do
  2. The catch → error management
  3. 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:

  1. Include null checks to ensure that the resource you are trying to clean up was created.
  2. 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

  1. The BufferedReader is declared and instantiated in the parentheses of the try, which binds it to the try block.
  2. The manual finally block is removed completely — Java handles everything automatically.
  3. 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:

ConceptDescription
ExceptionsUsed to report errors; eliminate the need for error codes or error flags.
Try blockContains the normal code, the work we are trying to do.
Catch blockContains error handling code. Runs only if a matching exception is thrown.
finally blockContains the cleanup code. Runs in all cases (success or exception).
AutoCloseableInterface indicating that a type supports automatic cleanup via the close() method.
CloseableInterface inheriting from AutoCloseable — most common for types using resources.
try-with-resourcesSyntax 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:

  1. How exceptions are represented by classes and organized in hierarchy
  2. checked exceptions and unchecked exceptions — understanding the difference helps write exception handling more effectively
  3. 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 Exception class descends from the Object class, but not directly — there is an intermediate class: the Throwable class. It is the Throwable class which provides the capabilities allowing the language to throw exceptions.
  • There are other classes that inherit from Throwable, notably the Error class. Errors generally represent serious problems on the JVM side from which we generally cannot recover.
  • The Exception class is a direct descendant of Throwable and 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:

  • IOException
  • FileNotFoundException (inherits from IOException)
  • 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)
  • ArrayIndexOutOfBoundsException
  • IllegalArgumentException

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 FileReaderUnhandled 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:

  • ArithmeticException is an unchecked exception (inherits from RuntimeException)
  • 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 Exception to 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:

  1. methodC throws an exception and does not handle it
  2. The exception starts moving up the call stack
  3. It arrives in methodB which does not handle it either
  4. It arrives in methodA which has a catch → the exception is caught
  5. methodC and methodB are 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:

  1. Put a try/catch in processFile to handle IOException
  2. Propagate the exception to the calling method by adding throws IOException to the signature of processFile

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

ConceptDescription
Exceptions represented by classesOrganized in a hierarchy, all inheriting from Exception.
Multiple catchesA try can have several catchs, tested from top to bottom. Order: more specific first.
Checked exceptionsThe compiler throws an error if not handled. Inherit from Exception but NOT from RuntimeException.
Unchecked exceptionsThe compiler does not require management. Inherit from RuntimeException.
Absolute ruleAny unhandled exception at runtime crashes the application, whether checked or unchecked.
Exceptions and methodsAn exception can cross method boundaries up the call stack.
Throws clauseA 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:

  1. Throwing an exception from our code — How to indicate that an error has occurred
  2. Create custom exceptions — Java allows you to define custom exception types
  3. 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:

  1. In the Project window, right-click on the package name
  2. New → Java Class
  3. Name: InvalidStatementException
  4. The class is created as a normal class
  5. Add extends Exception to make it a real exception class
  6. 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:

  1. performOperation throws InvalidStatementException → should declare throws InvalidStatementException
  2. processFile calls performOperationprocessFile must also declare throws IOException, InvalidStatementException
  3. main() calls processFilemain() must handle InvalidStatementException with 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:

  1. We wrap the entire body of the method in a try block
  2. We first catch InvalidStatementException and rethrow it as is (so as not to wrap it in itself)
  3. We catch Exception and wrap it in an InvalidStatementException with 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 InvalidStatementException message (high-level context)
  • The cause of type NumberFormatException (detailed underlying exception)

4.10 Module 4 Summary

ConceptDescription
throwKeyword to throw an exception from our code.
newOperator used to create an instance of the exception before throwing it.
Exception creationthrow new TypeException("description");
Custom exceptionClass that inherits from Exception (directly or indirectly). If directly inherits from Exception → checked exception.
Members of a custom exceptionGenerally only the appropriate constructors are needed.
Throws clauseAny method that does not handle a checked exception must declare it in its throws clause.
Chaining exceptionOne exception can wrap another. Allows you to throw a high-level exception while preserving the original exception.
initCauseMethod 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:

  1. Organization of exception handling — Where do we place catch blocks in the code?
  2. What to do with exception information — The answer depends on who you report this information to.
  3. 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.err is 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

HearingInformation providedDestination
End userClear message + problematic lineSystem.out (screen)
Developers / SupportMessage + cause + complete stack traceSystem.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:

  1. 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.

  2. 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).

  3. Exceptions used to pass standard data — Exceptions carry leftVal and rightVal data 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

ModuleMain changes
02 — beforeManual management of the reader (without try-with-resources), a single catch Exception
02 — afterConversion to try-with-resources
03 — afterAdded multiple catches (FileNotFoundException, IOException, Exception), processFile method extracted with throws clause
04 — afterAdded InvalidStatementException, checking number of parts, cascading throws, exception chaining
05 — afterMoved 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 → Addition
  • subtract → Subtraction
  • multiply → Multiplication
  • divide → 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.