DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Optimizing Natural Language Queries for Multi-Service Information Retrieval
  • Mastering Async Context Manager Mocking in Python Tests
  • Designing Scalable Java APIs With GraphQL
  • Optimizing Fine-Grained GraphQL Access Control and Query Performance

Trending

  • Why Documentation Matters More Than You Think
  • Subtitles: The Good, the Bad, and the Resource-Heavy
  • Scaling Mobile App Performance: How We Cut Screen Load Time From 8s to 2s
  • How to Build Scalable Mobile Apps With React Native: A Step-by-Step Guide
  1. DZone
  2. Data Engineering
  3. Databases
  4. Error Handling in Spring for GraphQL

Error Handling in Spring for GraphQL

Let's discuss error handling in Spring for GraphQL. We will also look at the ErrorHandler implementation that's capable of handling the custom and built-in exceptions.

By 
Ion Pascari user avatar
Ion Pascari
·
Aug. 31, 22 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
19.4K Views

Join the DZone community and get the full member experience.

Join For Free

The Problem

Recently, I wrote some GraphQL endpoints and got a bit blocked when I came to the error handling mechanism. Usually, when writing REST endpoints, you either go for a particular @ExceptionHandler for your controller or you go for the @ControllerAdvice to handle exceptions globally for multiple controllers. Apparently, that is not the case for GraphQL. There is a completely different approach for handling errors.

First, the most important thing that I should mention is that I am using:

implementation("org.springframework.boot:spring-boot-starter-graphql")

And not:

implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:14.0.0")

These are two completely different things, and this should be kept in mind during development and research on different websites. 

So what is the problem? Whenever you run a GraphQL query/mutation and your service/facade is throwing an exception — let's say a NotFoundException — by default, you’re getting this output for the result:

JSON
 
{
  "errors": [
    {
      "message": "INTERNAL_ERROR for 2ce1d7be-86f2-da5d-bdba-aac45f4a534f",
      "locations": [
        {
          "line": 1,
          "column": 13
        }
      ],
      "path": [
        "deleteCourseById"
      ],
      "extensions": {
        "classification": "INTERNAL_ERROR"
      }
    }
  ],
  "data": {
    "deleteCourseById": null
  }
}

Meh, that is not intuitive at all! We miss the exception message, right? This needs to be fixed. I want to be able to provide the exception message, and in certain scenarios, be able to override the exception message for some exceptions and display it.

My biggest mistake was to google it straightaway instead of going through the documentation first. That led me to a journey of trial and errors as I’ve never seen before, and all of that is because most of the research ecosystem is filled with QA and tutorials for the com.graphql-java-kickstart:graphql-spring-boot-starter library or io.leangen.graphql library, and very little is to be found about Spring for GraphQL. There are lots of valid answers about the error handling either by implementing the GraphQLError or by implementing a custom GraphQLErrorHandler or by enabling some kind of property and so on, but none of them work in Spring for GraphQL, as it is a completely different library.

Epiphany

After trying everything out, let’s see what the documentation states about exception resolution:

  DataFetcherExceptionResolver is an asynchronous contract. For most implementations, it would be sufficient to extend DataFetcherExceptionResolverAdapter and override one of its resolveToSingleError or resolveToMultipleErrors methods that resolve exceptions synchronously.

Wow, how simple is that? Lesson learned. Always check documentation first!

In order to demonstrate the error handling in Spring for GraphQL, let’s configure a mini project about courses and instructors. For this purpose I used Kotlin, but the solution would work in Java as well. For the sake of conciseness lots of classes won’t be shown here, but you can go ahead and take a look at the full source code on GitHub. Here are the DTOs being used:

Kotlin
 
data class CourseRequest(
    @get:NotBlank(message = "must not be blank") val name: String,
    @get:NotBlank(message = "must not be blank") val category: String,
    val instructor: InstructorRequest
)

data class CourseResponse(
    val id: Int?,
    val name: String,
    val category: String,
    val createdAt: String,
    val updatedAt: String,
    val instructor: InstructorResponse
)

data class InstructorRequest(
    @get:NotBlank(message = "must not be blank") val name: String,
)

data class InstructorResponse(
    val id: Int?,
    val name: String?,
)

And here is their representation in the schema.graphqls:

JSON
 
type CourseResponse {
    id: ID
    name: String
    category: String
    instructor: InstructorResponse
}

input CourseRequest{
    name: String
    category: String
    instructor: InstructorRequest
}

type InstructorResponse {
    id: ID
    name: String
}

input InstructorRequest {
    name: String
}

Now we have our controller:

Kotlin
 
@Controller
class CourseGraphQLController(val courseFacade: CourseFacade) {

    @QueryMapping
    fun getCourseById(@Argument id: Int): CourseResponse = courseFacade.findById(id)

    @QueryMapping
    fun getAllCourses(): List<CourseResponse> = courseFacade.findAll()

    @SchemaMapping(typeName = "CourseResponse", field = "instructor")
    fun getInstructor(course: CourseResponse): InstructorResponse = course.instructor

    @MutationMapping
    fun deleteCourseById(@Argument id: Int) = courseFacade.deleteById(id)

    @MutationMapping
    fun createCourse(@Valid @Argument request: CourseRequest): CourseResponse = courseFacade.save(request)
}

Just for the sake of mentioning, Spring for GraphQL is merely providing support for GraphQL Java in more opinionated way — an annotation-based approach. So instead of implementing GraphQLQueryResolver/GraphQLMutationResolver, we use @QueryMapping and @MutationMapping alongside with @Argument to resolve the method arguments. Also there is @SchemaMapping (@QueryMapping/@MutationMapping’s parent) which allows a method to act as the DataFetcher for a field from the schema mapping.  

Okay, here is the schema mapping for the queries/mutations:

JSON
 
type Query {
    getAllCourses: [CourseResponse]!
    getCourseById(id: Int): CourseResponse
}

type Mutation {
    deleteCourseById(id: Int): Boolean
    createCourse(request: CourseRequest): CourseResponse
}

In order to get a little context about the errors, here is my generic NotFoundException thrown from the service:

Kotlin
 
class NotFoundException(clazz: KClass<*>, property: String, propertyValue: String) :
    RuntimeException("${clazz.java.simpleName} with $property equal to [$propertyValue] could not be found!")

So by running the following GraphQL query:

JSON
 
query { getCourseById(id: -999) {
    id
    name
    instructor {
        id
    }
}}

I was expecting to get something like "Course with id equal to [-999] could not be found!" But that was not the case, as we’ve seen at the beginning. 

Solution

Okay, enough talk; time to fix this. Here is the required subclass, according to the documentation:

Kotlin
 
@Component
class GraphQLExceptionHandler : DataFetcherExceptionResolverAdapter() {
    companion object {
        private val log: Logger = LoggerFactory.getLogger(this::class.java)
    }

    override fun resolveToSingleError(e: Throwable, env: DataFetchingEnvironment): GraphQLError? {
        return when (e) {
            is NotFoundException -> toGraphQLError(e)
            else -> super.resolveToSingleError(e, env)
        }
    }
    private fun toGraphQLError(e: Throwable): GraphQLError? {
        log.warn("Exception while handling request: ${e.message}", e)
        return GraphqlErrorBuilder.newError().message(e.message).errorType(ErrorType.DataFetchingException).build()
    }
}

So we extended the DataFetcherExceptionResolverAdapter and overrode the resolveToSingleError method to treat our exception the correct way. Basically, it is a translation of the NotFoundException to GraphQLError.  Now, if we run our query again:

JSON
 
{
  "errors": [
    {
      "message": "Course with id equal to [-999] could not be found!",
      "locations": [],
      "extensions": {
        "classification": "DataFetchingException"
      }
    }
  ],
  "data": {
    "getCourseById": null
  }
}

Beautiful, isn’t it?

But wait; there is more. This here is a custom exception. What about some built-in exceptions like the ConstraintViolationException, which is thrown when the @Valid is invalidated? As you’ve seen my CourseRequest’s name is annotated with @NotBlank:

Kotlin
 
data class CourseRequest(
    @get:NotBlank(message = "must not be blank") val name: String,
    @get:NotBlank(message = "must not be blank") val category: String,
    val instructor: InstructorRequest
)

What happens when I try to create a Course with an empty name, like this?

JSON
 
mutation { createCourse(
    request: {
        name: "",
        category: "DEVELOPMENT",
        instructor: {
            name: "Thomas William"
        }
    }) {
    id
    name
}}

Oh God, no… Again, that INTERNAL_ERROR message... 

But no worries — with our GraphQLExceptionHandler in place, it is a matter of adding a new exception to be handled. Also, just for safety, I’ll add the Exception there too, as the times comes new specializations can be added, but by default for untreated exception the exception message always will be shown. So here is our new implementation:

Kotlin
 
@Component
class GraphQLExceptionHandler : DataFetcherExceptionResolverAdapter() {
    companion object {
        private val log: Logger = LoggerFactory.getLogger(this::class.java)
    }

    override fun resolveToSingleError(e: Throwable, env: DataFetchingEnvironment): GraphQLError? {
        return when (e) {
            is NotFoundException -> toGraphQLError(e)
            is ConstraintViolationException -> handleConstraintViolationException(e)
            is Exception -> toGraphQLError(e)
            else -> super.resolveToSingleError(e, env)
        }
    }

    private fun toGraphQLError(e: Throwable): GraphQLError? {
        log.warn("Exception while handling request: ${e.message}", e)
        return GraphqlErrorBuilder.newError().message(e.message).errorType(ErrorType.DataFetchingException).build()
    }

    private fun handleConstraintViolationException(e: ConstraintViolationException): GraphQLError? {
        val errorMessages = mutableSetOf<String>()
        e.constraintViolations.forEach { errorMessages.add("Field '${it.propertyPath}' ${it.message}, but value was [${it.invalidValue}]") }
        val message = errorMessages.joinToString("\n")
        log.warn("Exception while handling request: $message", e)
        return GraphqlErrorBuilder.newError().message(message).errorType(ErrorType.DataFetchingException).build()
    }
}

As you can see, the NotFoundException/Exception will be simply translated to GraphQLError (yes, at the moment, the logic’s the same and NotFoundException may be removed, but I prefer to keep them separated for future possible changes). ConstraintViolationException is treated separately by constructing a sensible message.

Now, if we run our mutation again, voila!

JSON
 
{
  "errors": [
    {
      "message": "Field 'createCourse.request.name' must not be blank, but value was []",
      "locations": [],
      "extensions": {
        "classification": "DataFetchingException"
      }
    }
  ],
  "data": {
    "createCourse": null
  }

Conclusion

In this article, we discussed error handling in Spring for GraphQL and we looked at the implementation of ErrorHandler that is capable of handling both the custom exception and the built-in exceptions. And we learned an important lesson: Always check the documentation first!

That’s all folks; hope that you liked it. In case you missed it, here is the full project. 

P.S. Here is an unrelated tip for the Kotlin users who are still trying to implement the GraphQLError and extend the RuntimeException and getting the “Accidental override: The following declarations have the same JVM signature (getMessage()Ljava/lang/String;)”. The dirty workaround is to have it implemented in Java and have a single Java class in a 100% Kotlin project. The elegant workaround is to extend the newly created GraphqlErrorException specifically created for Kotlin users, as per the opened GitHub issue. 

GraphQL

Opinions expressed by DZone contributors are their own.

Related

  • Optimizing Natural Language Queries for Multi-Service Information Retrieval
  • Mastering Async Context Manager Mocking in Python Tests
  • Designing Scalable Java APIs With GraphQL
  • Optimizing Fine-Grained GraphQL Access Control and Query Performance

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!