Techies.

Exception handling with SpringBot

This article explores how SpringBot handles exceptions


Exceptions in SpringBot are handled at two levels:

  • Pre-filter
  • Post-filter

Pre-filter

Pre-filter exception handling happens before authentication and before reaching GraphQL.

Exceptions that occur pre-filter are not handled by GraphQL so require custom JSON so that the client-side can respond appropriately.

Post-filter

The majority of exceptions occur post-filter. Post-filter exceptions are related to queries, mutations and all the associated underlying services and other related classes.

Post-filter exceptions are handled by GraphQL.

Post-filter Exception hierarchy

All post filter exceptions are caught and bubbled up to serverside/src/main/java/[ProjectName]/graphql/CustomGraphQLErrorHandler.java. Within this class, the handleDataFetchingError method catches all the exceptions and transfers them to appropriate GraphQLError implementation to be be parsed to the client-side for handling.

The CustomGraphQLErrorHandler is a global handler for all post-filter requests.

Errors caught while performing a query or mutation using GraphQL are the type ExceptionWhileDataFetching. This exception type is a wrapper for other exceptions.

If required, the wrapped exception can be sourced using exceptionWhileDataFetching.getException().

For example, see the top of the handleDataFetchingError method:

private Stream<GraphQLError> handleDataFetchingError(ExceptionWhileDataFetching error) {
        final Throwable exception = error.getException();
        GraphQLError processedException = error;
        ...

The handleException method will take in the GraphQLError thrown by the application before transfering that to the appropriate format exception, and returning its back. The return value is a Stream, as one request may have mutliple errors (e.g. validation).

More detail on this can found in the GraphQL Java - Excecution Docs.

It is important to implement at least part of the GraphQLErrorHandler interface, so that useful information can be returned to the client-side. Examples of useful override methods are shown later in this article.

Best Practices of Exception

Exceptions should never swallowed

It is important to handle all errors and not hide them within the code.

For example, the following is called swallowing exceptions and is considered bad practice:

try {
    // Code that throws an exception
} catch (Exception e) {
    // Nothing here
}

Another example of swallowing exceptions:

try {
    // Code that throws an exception
} catch (Exception e) {
    throw new CustomException("Something went wrong");
}

This is bad because it discards all the data (i.e the stack trace) of the original exception.

The correct approach to handling exceptions is to either deal with them in the moment or delegate them to a upper level class.

An example of delegation would be as follows:

try {
    // Code that throws an exception
} catch (Exception e) {
    // Put back the original exception when throw the exception
    throw new CustomException("Something went wrong", e);
}

This allows the details of the original exception to be maintained and the actual handling of the exception to be managed by an upper level exception handler.

The correct handling of exceptions is necessary to ensure your code can be debugged easily and errors do not occur without your knowledge. It is also important to log errors as they occur.

Runtime vs. checked exceptions

Checked exceptions, as the name suggests, are exceptions that checked by the compiler.

Checked exceptions are caught at compile time and must be handled before correctly running your application.

Unchecked exceptions might still be shown as warnings during compile time, but will not prevent your application from building. You can read more about this here.

We recommend using typed checked exceptions as they provide a mechanism for ascertaining the cause of an exception within the GraphQL error handler. This extra information can allow us to provide more meaningful feedback to the user.

You will not be able to return useful error messages for unchecked runtime exceptions.

Of course, you should aim to prevent exceptions from occuring at all where reasonable - exceptions should be exceptional.

Exceptions should be exceptional

To be proactive in preventing possible exceptions, check for issues before they can cause an exception.

Common exceptions that can be avoided include NullPointerException and IndexOutOfBoundsException.

An example of proactively avoiding exceptions is to validate assumptions before your business logic is performed.

Good example:

if (obj != null) {
    // Business logic on obj.
}

Bad example:

try {
    // Business logic on obj
} catch(NullPointerException e){
    ...
}

Adding custom GraphQLErrors and exceptions

For the above to work you will have to create the following custom exception and handle it in the GraphQLErrorHandler before having it returned to the client-side.

1. Custom Exception

We suggest you place custom exceptions in an exceptions package within the services package (serverside/src/main/java/[ProjectName]/services/exceptions/). Create a Ja class named CustomException.java.

public class CustomException extends Exception {

    private String attribute;

    public CustomException() {
        super();
    }

    public CustomException(String message, String attribute) {
        super(message);
        this.attribute = attribute;
    }

    public CustomException(String message) {
        super(message);
    }

    public CustomException(String message, Throwable cause) {
        super(message, cause);
    }

    public CustomException(Throwable cause) {
        super(cause);
    }

    protected CustomException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public String getAttribute() {
        return this.attribute;
    }
}

2. Custom GraphQLError

You will need to create an implementation of GraphQLError so that GraphQL can return an error message in a format that the client-side is expecting. See Graphql Error

It is also important to filter and sanitise exceptions before returning them to the client-side so that no sensitive data or details about the server-side are leaked to users or attackers.

GraphQLErrorBusiness classes belong in the graphql.utils.errors package.

Create a file named:
serverside/src/main/java/[ProjectName]/graphql/utils/errors/CustomGraphQLError.java and paste the following code:

public class CustomGraphQLError implements GraphQLError {
    private String customAttribute;

    private String errorMessage;

    /**
    * The error message to be shown on the client side
    * Make sure that this is sanitized and will not contain sensitive data
    * We highly recommend you to implement this method, to make sure the error messages are clear when return to the client side.
    */
    @Override
    public String getMessage() {
        return errorMessage;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null;
    }

    /**
    * The type of the error. You can choose from ErrorType Enums.
    * This may be useful for the clientside to decide how to deal with the error
    */
    @Override
    public ErrorType getErrorType() {
        return ErrorType.ExecutionAborted;
    }

    @Override
    public List<Object> getPath() {
        return null;
    }

    @Override
    public Map<String, Object> toSpecification() {
        return null;
    }

    /**
     * The extensions are simply key-value map
     * This would be return as json object to the client side
     * We recommend to put the status code here so that client side can handle the status code appropriately.
     */
    @Override
    public Map<String, Object> getExtensions() {
        Map<String, Object> extensions = new HashMap<>();
        extensions.put("statusCode", 420);
        extensions.put("fieldName", this.customAttribute);
        return extensions;
    }

    /**
    * The constructor for the error
    * Takes a customException and gets the attribute from it
    * so that we can easily create a customgraphqlerror from it
    */
    public CustomGraphQLError(CustomException customException) {
        this.customAttribute = customException.getAttribute();
        this.errorMessage = this.customAttribute + " has a custom error";
    }
}

3. Catch the exception and return the GraphQLError

We must take an exception and return a Stream of a class which implements GraphQLError so that GraphQL can return an expected response type to the client-side.

To do this we need to modify the handleDataFetchingError method which is found in the
serverside/src/main/java/[ProjectName]/graphql/CustomGraphQLErrorHandler.java file. This method checks the type of the wrapped exception and handles them apropriately.

if (exception instanceof AccessDeniedException) {
    processedException = new GraphQLAuthorisationError();
} else if (exception instanceof AuthenticationException) {
    processedException = new GraphQLAuthenticationError();
}

Within that method in the CustomGraphQLErrorHandler class, find the protected region named: Handle other Exception Here. You can turn this on and add another condition.

...
    } else if (exception instanceof DataIntegrityViolationException) {
        processedException = handleDataIntegrationError((DataIntegrityViolationException) exception);
    }

    // % protected region % [Handle other Exception Here] on begin
    //Remember to turn on the protected region    --->    ^^^
    else if (exception instanceof CustomException) {
        return handleCustomException((CustomException) exception);
    }
    // % protected region % [Handle other Exception Here] end

    return Stream.of(processedException);
}

To keep this method clean, we should create a custom exception handler method for exceptions that require multiple lines. To do this we will create a custom handler in the protected region at the bottom of the class.

// % protected region % [Add any additional class methods here] on begin
Stream<GraphQLError> handleCustomException(CustomException customException) {
    return Stream.of(new CustomGraphQLError(customException));
}
// % protected region % [Add any additional class methods here] end

You can handle the exception however you like, as long as you return a Stream<GraphQLError> type. In this case, we create a new CustomGraphQLError which takes data from a given CustomException and shows a returns an error message saying that custom attribute has a custom error.

4. Throwing the custom exception

We can test that this exception works by throwing it from the update method of an entity service.

For the sake of showing how exceptions work in this article, the following steps instruct you to update code outside of a protected region. These changes will not persist and will be removed the next time your bot rebuilds your application.

  1. Select a service class from serverside/src/main/java/[ProjectName]/services/.
  2. Find the update method. It will have a signature of the form public Entity update(Entity entity)
  3. Add the custom exception to the update method signature:

    @Override
     @Transactional
     @PreAuthorize("hasPermission('CustomEntity', 'update')")
     public CustomEntity update(CustomEntity entity) throws CustomException {
         ...
    
  4. Throw your exception at the entry to your update method.

    @Override
     @Transactional
     @PreAuthorize("hasPermission('CustomEntity', 'update')")
     public CustomEntity update(CustomEntity entity) throws CustomException {
         throw new CustomException("Test exception message", "Test attribute name");
         ...
     }
    
  5. Now update the AbstractService.java update method to also throw our CustomException.

    public abstract E update (E entity) throws CustomException;
    
  6. Add the exception to the method signature of your EntityMutationResolver.java which can be found under serverside/src/main/java/[Project Name/graphql/resolvers/.

    public Entity updateEntity (Entity rawEntity) throws CustomException {
    ...
    
  7. Run your application and attempt an update action through the client-side on the your selected service entity.

    You should see your new custom exception message and attribute name shown in a toast message from your client-side.

Java GraphQL Error Handling

Last updated: 10 January 2020


Related articles