DEV Community

loading...
Cover image for Handling Exceptions and Errors in Play Framework

Handling Exceptions and Errors in Play Framework

pazvanti
Originally published at petrepopescu.tech ・4 min read

Article originally posted on my personal blog under (How to handle Exception in Play Framework)[https://petrepopescu.tech/2021/03/handling-exceptions-and-errors-in-play-framework/]

When programming, it is important to always take into consideration exceptions. No matter how well your code is, there can always be invalid data summited by the user problems in other libraries that can trigger exceptions. Also, using this mechanism is an easy way of terminating invalid flows as soon as possible. Exceptions are normal and should be treated in all services. Even in case of an exception, we must provide a response to the user so that he knows the request was indeed processed but something was not right. In this article, I will present how to handle exceptions in Play Framework and how to respond to the client.

The HttpErrorHandler

Play Framework provides the HttpErrorHandler interface that must be implemented by all exception handlers. This interface has two methods and both of them need to be properly implemented. One deals with errors from the client (like an invalid endpoint is accessed), while the other handles exceptions that happened on the server side.

public class ErrorHandler implements HttpErrorHandler {
    @Override
    public CompletionStage<Result> onClientError(Http.RequestHeader request, int statusCode, String message) {
        // Implementation
    }

    @Override
    public CompletionStage<Result> onServerError(Http.RequestHeader request, Throwable exception) {
        // Implementation
    }
}
Enter fullscreen mode Exit fullscreen mode

For the client error, let’s just return a Json String that holds the status code and the error message. The onClientError method provides all the needed information and we can return a CompletebleFuture as the result.

public class ErrorHandler implements HttpErrorHandler {
    @Override
    public CompletionStage<Result> onClientError(Http.RequestHeader request, int statusCode, String message) {
        Map<String, Object> response = new HashMap<>();
        response.put("status", statusCode);
        response.put("error", message);
        return CompletableFuture.completedFuture(
                Results.status(statusCode, Json.toJson(response))
        );
    }

    @Override
    public CompletionStage<Result> onServerError(Http.RequestHeader request, Throwable exception) {
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

The server error is where most of the magic happens. All exceptions that are not caught in our code will eventually reach this method so it is important that we properly handle everything here. We must log the exception and return a proper response back to the client. As in the previous method, we will be returning a Json String. However, how this string is obtained can be a bit more complex.

I usually define one or more custom exceptions that hold the needed information. For this example I will make a PlatformException that I will throw in certain parts of the service to signal that no further processing is needed and to return a response back to the client.

public class PlatformException extends Exception {
    private String message;
    private String errorCode;

    public PlatformException(String message, String errorCode) {
        this.message = message;
        this.errorCode = errorCode;
    }

    public PlatformException(String message, String errorCode, Throwable throwable) {
        super(throwable);
        this.message = message;
        this.errorCode = errorCode;
    }

    // Getters
}
Enter fullscreen mode Exit fullscreen mode

Now, in our error handler, when this exception is encountered, we will return a 500 response with the JSON string obtained from this error. If it is not our custom exception, we build one just to make it easier to return the message in a standard format all the time.

public class ErrorHandler implements HttpErrorHandler {
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Override
    public CompletionStage<Result> onClientError(Http.RequestHeader request, int statusCode, String message) {
        Map<String, Object> response = new HashMap<>();
        response.put("status", statusCode);
        response.put("error", message);
        return CompletableFuture.completedFuture(
                Results.status(statusCode, Json.toJson(response))
        );
    }

    @Override
    public CompletionStage<Result> onServerError(Http.RequestHeader request, Throwable exception) {
        LOGGER.error("Error encountered.", exception);
        Throwable exceptionToReturn = exception;
        if (!(exception instanceof PlatformException)) {
            exceptionToReturn = new PlatformException(exception.getMessage(), "UNKNOWN", exception);
        }
        return CompletableFuture.completedFuture(
                Results.internalServerError(Json.toJson(exceptionToReturn))
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Last but not least, we need to inform Play about our error handler. To do this we edit the application.conf file and set the play.http.errorHandler configuration variable to our class name:

play.http.errorHandler = "ErrorHandler"
Enter fullscreen mode Exit fullscreen mode

What! Full Stack Trace is returned?

If you were to run this code you would see that the JSON includes the full stack trace. This is not desirable since it can provide too much information. We don’t need the client to know all this, so let’s improve our code. First, we don’t make the JSON out of the exception itself. Instead, we make and ErrorDTO that holds the message, the error code and what other information we need. Now, when the exception happens, only the minimum needed information is returned.

public class ErrorHandler implements HttpErrorHandler {
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Override
    public CompletionStage<Result> onClientError(Http.RequestHeader request, int statusCode, String message) {
        Map<String, Object> response = new HashMap<>();
        response.put("status", statusCode);
        response.put("error", message);
        return CompletableFuture.completedFuture(
                Results.status(statusCode, Json.toJson(response))
        );
    }

    @Override
    public CompletionStage<Result> onServerError(Http.RequestHeader request, Throwable exception) {
        LOGGER.error("Error encountered.", exception);
        ErrorDTO error;
        if (exception instanceof PlatformException) {
           errorDTO = new ErrorDTO(exception.getMessage(), ((PlatformException) exception).getErrorCode());
        } else {
            errorDTO = new ErrorDTO(exception.getMessage(), "UNKNOWN");
        }
        return CompletableFuture.completedFuture(
                Results.internalServerError(Json.toJson(errorDTO))
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Article originally posted on my personal blog under (How to handle Exception in Play Framework)[https://petrepopescu.tech/2021/03/handling-exceptions-and-errors-in-play-framework/]

Discussion (0)