Mastering Exception Messages: Named Constructors for Clean Code
💡 Every developer has written error-handling code like this at some point:
if (status.isCompleted()) {
throw new OrderException("Order cannot be cancelled");
}
At first glance, this seems fine. But what if we need to add a new status condition, for example, the order should not be expired? After a brief struggle with our inner perfectionist, we might write something like this:
if (status.isExpired()) {
throw new OrderException("Order cannot be cancelled");
}
Eventually, a new condition appears, and this time our inner code cleanliness advocate won’t allow us to copy-paste and fix the condition. As a result, we might write something like this:
public class OrderException extends RuntimeException {
public OrderException(String orderId) {
super("Order %s cannot be cancelled".formatted(orderId));
}
}
⚠️ Not ideal, but okay.
The client code looks a bit better now:
if (status.isFinished()) {
throw new OrderException(orderId);
}
⏳ Time passes, and now the error message needs to depend on the situation. The current implementation doesn’t support different error messages. We could rename the current exception class to OrderCancellationException
and create new classes accordingly, but for even a middle-sized project, there could be hundreds of such classes.
💡 There is a solution to this problem, and it’s quite simple. Let’s talk about a useful pattern that makes our code cleaner and more readable — named constructors.
🔹 What are Named Constructors?
Named constructors are static methods that return instances of a class. They help avoid creating multiple overloaded constructors.
🔹 Why is this important for exceptions?
When creating custom exceptions, we often need to pass various parameters. Named constructors clearly specify what data is being passed, making the code more readable.
👇 Example Usage
public class OrderException extends RuntimeException {
public static OrderException becauseOrderCannotBeCancelled(String orderId) {
return new OrderException("Order %s cannot be cancelled".formatted(orderId));
}
public static OrderException becauseProductIsOutOfStock(String orderId, String productId) {
return new OrderException("Order %s cannot be completed because product %s is out of stock".formatted(orderId, productId));
}
private OrderException(String message) {
super(message);
}
}
👇 How it looks in the code
public class Order {
public void cancel() {
if (this.status.isCompleted()) {
throw OrderException.becauseOrderCannotBeCancelled(id);
}
}
}
🔹 Advantages of this approach
- Readability: Named constructors make the code more understandable.
- Flexibility: It’s easier to add new ways of creating instances without the need for new overloaded constructors.
- Maintainability: The code becomes easier to maintain and extend.
🎯 Named constructors are an excellent way to make your code cleaner and more readable.