The checked exception is an Open/Closed Principle violation. If you throw a checked exception from a method in your code and the catch is three levels above, you must declare that exception in the signature of each method between you and the catch. This means that a change at a low level of the software can force signature changes on many higher levels. The changed modules must be rebuilt and redeployed, even though nothing they care about changed.
Encapsulation is broken because all functions in the path of a throw must know about the details of that low-level exception.
Checked exceptions can sometimes be useful if you are writing a critical library: You must catch them. But in general application development, the dependency costs outweigh the benefits.
classUserNotFoundExceptionextendsException { // Checked exceptionpublicUserNotFoundException(String message) { super(message); }}classAuthExceptionextendsException { // Checked exceptionpublicAuthException(String message) { super(message); }}publicclassAuthController {privateAuthService authService;publicvoidlogin(AuthDto dto) {try {authService.auth(dto); } catch(UserNotFoundException ex) { } catch(AuthException ex) { } }}publicclassAuthService {privateAuthService authService;// Must rethrow or catch a checked exceptionpublicvoidlogin(AuthDto dto) throwsUserNotFoundException,AuthException { User user =getUserByEmail(dto.getEmail());if(notMatch(user.getPasword(),dto.getPassword()) {thrownewAuthException("User does not exist"); }// Login success }}publicclassUserService throws UserNotFoundException {privateUserRepository userRepository;publicUsergetUserByEmail(String email) throwsUserNotFoundException {User user =userRepository.getUserByEmail(email);if (user ==null) {thrownewUserNotFoundException("User does not exist"); }return user; }}
Each exception that you throw should provide enough context to determine the source and location of an error.
classUserNotFoundExceptionextendsRuntimeException { // Unchecked exceptionpublicUserNotFoundException(String message) { super(message); }}classAuthExceptionextendsRuntimeException { // Unchecked exceptionpublicAuthException(String message) { super(message); }}publicclassAuthController {privateAuthService authService;publicvoidlogin(AuthDto dto) {try {authService.auth(dto); } catch(AuthException ex) {log.error("Auth failed", ex); } }}publicclassAuthService {privateAuthService authService;publicvoidlogin(AuthDto dto) {try {User user =getUserByEmail(dto.getEmail());if(notMatch(user.getPasword(),dto.getPassword()) {thrownewAuthException("User does not exist"); }// Login success } catch (UserNotFoundException ex) {thrownewAuthException("Can not get user"); // Missing stack trace from root exception } }}publicclassUserService throws UserNotFoundException {privateUserRepository userRepository;publicUsergetUserByEmail(String email) {User user =userRepository.getUserByEmail(email);if (user ==null) {thrownewUserNotFoundException("User does not exist"); }return user; }}
classAuthExceptionextendsRuntimeException { // Unchecked exceptionpublicAuthException(String message,Throwable cause) { super(message, cause); // Accept stack trace from root exception }}publicclassAuthService {privateAuthService authService;publicvoidlogin(AuthDto dto) {try {User user =getUserByEmail(dto.getEmail());if(notMatch(user.getPasword(),dto.getPassword()) {thrownewAuthException("User does not exist"); }// Login success } catch (UserNotFoundException ex) {thrownewAuthException("Can not get user", ex); // Provide the cause of exception } }}
Define Exception Classes in Terms of a Caller’s Needs
We should simplify our code considerably by wrapping the API that we are calling and making sure that it returns a common exception type.
It’s easy to say that the problem with the code above is that it is missing a null check, but in actuality, the problem is that it has too many. If you are tempted to return null from a method, consider throwing an exception or returning a SPECIAL CASE object instead.
Consider returning an empty list instead of a null value
Don’t Pass Null
Returning null from methods is bad, but passing null into methods is worse. Unless you are working with an API which expects you to pass null, you should avoid passing null in your code whenever possible.
We’ll get a NullPointerException if a caller passes a null parameter.
How can we fix it? We could create a new exception type and throw it:
publicdoublexProjection(Point p1,Point p2) {if (p1 ==null|| p2 ==null) {throwInvalidArgumentException("Invalid argument for MetricsCalculator.xProjection"); }return (p2.x –p1.x) *1.5;}// alternativepublicdoublexProjection(Point p1,Point p2) {assert p1 !=null:"p1 should not be null"; // throws AssertionExceptionassert p2 !=null:"p2 should not be null"; // throws AssertionExceptionreturn (p2.x –p1.x) *1.5;}
It’s good documentation, but it doesn’t solve the problem. If someone passes null, we’ll still have a runtime error.
In most programming languages there is no good way to deal with a null that is passed by a caller accidentally. Because this is the case, the rational approach is to forbid passing null by default. When you do, you can code with the knowledge that a null in an argument list is an indication of a problem, and end up with far fewer careless mistakes.