Custom Exception

In the article Clean Code with Exception, I discussed how using Exception can make code cleaner. However, it was missing a complete practical example and guidance on using custom Exception to make exceptions more meaningful and informative.

The requirements are identical to the Clean Code with Exception article. I’ll summarize them here for convenience so you don’t have to switch back and forth ;))

Write a program that calculates and transfers salaries to company employees. From this requirement, we need to accomplish 3 tasks:

  1. Calculate salary for employees
  2. Transfer salary to employees
  3. Coordinate salary calculation and transfer

We already implemented #1 in Clean Code with Exception, but in a broader context, #3 needs to distinguish between the error types from #1 and #2. For example, #1 requires asking the user to re-enter data and halting execution, while #2 can be retried (the company transfers salaries through two banks, VCB and VTB; if VCB fails, retry with VTB) ;))

1. Calculate salary for employees

From the above analysis, a generic Exception will not satisfy our needs. We need to create a custom Exception:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class SalaryException extends Exception {

  private ErrorType codeType;

  public SalaryException(ErrorType errorType, String message) {
    super(message);
    this.codeType = errorType;
  }

  public ErrorType getCodeType() {
    return codeType;
  }
}

With an enum defining the error types:

1
2
3
4
5
6
public enum ErrorType {
  UNKNOWN_ERROR,
  CALC_SALARY_PARAMS_INVALID,
  TRANSFER_MONEY_PARAMS_INVALID,
  TRANSFER_MONEY_NOT_ENOUGH_MONEY,
}

From here, we update the salary calculation method slightly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class SalaryCalculator {

  public int calcSalary(int workingDay, int salaryPerDay) throws SalaryException {

    if (workingDay <= 0)
      throw new SalaryException(CALC_SALARY_PARAMS_INVALID, "WorkingDay less than or equal zero");

    if (salaryPerDay <= 0)
      throw new SalaryException(CALC_SALARY_PARAMS_INVALID, "SalaryPerDay less than or equal zero");

    return workingDay * salaryPerDay;
  }
}

2. Transfer salary to employees

Now let’s write classes supporting money transfer. Suppose we integrate with two banks: VietcomBank and VietinBank. Both banks will implement a common interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public interface Bank {

  void transferMoney(String accountNo, int amount) throws SalaryException;

  default void checkTransferParams(String accountNo, int amount) throws SalaryException {

    if (accountNo == null || accountNo.length() == 0)
      throw new SalaryException(TRANSFER_MONEY_PARAMS_INVALID, "AccountNo null or empty");

    if (amount <= 0)
      throw new SalaryException(TRANSFER_MONEY_PARAMS_INVALID, "Amount less than or equal zero");
  }
}

I also implemented parameter validation for money transfers via the checkTransferParams default method.

Implementation for VietcomBank:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class VietcomBank implements Bank {

  @Override
  public void transferMoney(String accountNo, int amount) throws SalaryException {
    checkTransferParams(accountNo, amount);

    if (amount > 10_000_000)
      throw new SalaryException(TRANSFER_MONEY_NOT_ENOUGH_MONEY, "VCB only support transfer amount less than 10M");

    System.out.println("VCB transfer success");
  }
}

To simulate a bank failure, I make VietcomBank throw an error when the amount exceeds 10M.

Similarly for VietinBank, but without errors:

1
2
3
4
5
6
7
8
9
public class VietinBank implements Bank {

  @Override
  public void transferMoney(String accountNo, int amount) throws SalaryException {
    checkTransferParams(accountNo, amount);

    System.out.println("VTB transfer success");
  }
}

3. Coordinate salary calculation and transfer

And finally, #3 coordinates salary calculation and transfer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SalaryTransfer {

  private final MoneyTransfer moneyTransfer = new MoneyTransfer();
  private final SalaryCalculator salaryCalculator = new SalaryCalculator();

  public void salaryTransfer(int workingDay, int salaryPerDay, String accountNo) throws SalaryException {

    int salary = salaryCalculator.calcSalary(workingDay, salaryPerDay);
    moneyTransfer.transferMoney(accountNo, salary);
    System.out.println("Transfer Salary success");

  }

  public static void main(String[] args) {
    SalaryTransfer salaryTransfer = new SalaryTransfer();

    try {
      salaryTransfer.salaryTransfer(Integer.parseInt(args[0]), Integer.parseInt(args[1]), args[2]);
    } catch (NumberFormatException e) {
      System.out.println("Parse int error " + e.getMessage());
    } catch (SalaryException e) {
      System.out.println(e.getMessage());
    }
  }
}

Here I wrote the main method directly so we can run and test right away. You can view the full example in this repo.

4. Build and test

Run the following command to build:

./gradlew clean build

and test:

java -cp build/libs/*.jar dev.trile.customexception.SalaryTransfer 11 1000000 EM1
Transfer 11000000 to EM1
VCB only support transfer amount less than 10M
Retry with VTB
VTB transfer success
Transfer Salary success
java -cp build/libs/*.jar dev.trile.customexception.SalaryTransfer -1 1000000 EM1
WorkingDay less than or equal zero
java -cp build/libs/*.jar dev.trile.customexception.SalaryTransfer 10 -1000000 EM1
SalaryPerDay less than or equal zero
java -cp build/libs/*.jar dev.trile.customexception.SalaryTransfer 15 1000000 EM1
Transfer 15000000 to EM1
VCB only support transfer amount less than 10M
Retry with VTB
VTB transfer success
Transfer Salary success

Conclusion: By using custom Exception, our code becomes clearer and more consistent when handling errors:

  • In the standard case, when an Exception is thrown, the flow is broken as in calcSalary and salaryTransfer
  • In cases where we have a specific recovery strategy for an Exception, we can try/catch and handle it, as in MoneyTransfer.moneyTransfer
updatedupdated2026-09-032026-09-03
Load Comments?