theJiBz
theJiBz

Reputation: 31

Why graphql-js executor stops resolving child fields that have their own resolvers when the parent resolver return null/undefined?

While writing a lib for GraphQL in JavaScript I stumbled upon a curious behavior. I managed to isolate it in a very simple example. Let's take this server snippet:


    const { ApolloServer, gql } = require("apollo-server")

    const typeDefs = gql`
      type Book {
        resolveItSelf: String
      }

      type Query {
        book: Book
      }
    `

    const resolvers = {
      Query: {
        book: () => {
          return null // same behavior with undefined here
        }
      },
      Book: {
        resolveItSelf: () => "resolveItSelf"
      }
    }

    const server = new ApolloServer({ typeDefs, resolvers })

    server.listen().then(({ url }) => {
      console.log(`🚀  Server ready at ${url}`)
    })

If we query this server with the following query:

    {
      book {
        resolveItSelf   
      }
    }

We get this result:

{
  "data": {
    "book": null
  }
}

So, I was expecting the graphql executor to try to resolve the "resolveItSelf" field (which have its own resolver) even if the book resolver returned null.

A way to get the behavior I expect is to change the book's resolver a little bit:

const resolvers = {
  Query: {
    book: () => {
      return {} // empty object instead of null/undefined
    }
  },
  Book: {
    resolveItSelf: () => "resolveItSelf"
  }
}

Then we get this result:

{
  "data": {
    "book": {
      "resolveItSelf": "resolveItSelf"
    }
  }
}

The field is resolved even if the parent is empty !

So my question is why the graphql-js executor stop trying to resolve fields if the parent's resolver return null/undefined, even though requested fields can be resolved on their own ? (Is there a section in the draft that cover this ?)

Upvotes: 1

Views: 1056

Answers (1)

Daniel Rearden
Daniel Rearden

Reputation: 84697

In GraphQL, null represents a lack of a value. If a field resolves to null, it doesn't make sense for its "child" field resolvers' to be called since they wouldn't be returned in the response anyway.

From the Value Completion section of the spec (emphasis mine):

  1. If the fieldType is a Non‐Null type:
    a. Let innerType be the inner type of fieldType.
    b. Let completedResult be the result of calling CompleteValue(innerType, fields, result, variableValues).
    c. If completedResult is null, throw a field error.
    d. Return completedResult.
  2. If result is null (or another internal value similar to null such as undefined or NaN), return null.
  3. If fieldType is a List type:
    a. If result is not a collection of values, throw a field error.
    b. Let innerType be the inner type of fieldType.
    c. Return a list where each list item is the result of calling CompleteValue(innerType, fields, resultItem, variableValues), where resultItem is each item in result.
  4. If fieldType is a Scalar or Enum type:
    a. Return the result of “coercing” result, ensuring it is a legal value of fieldType, otherwise null.
  5. If fieldType is an Object, Interface, or Union type:
    a. If fieldType is an Object type.
    i. Let objectType be fieldType.
    b. Otherwise if fieldType is an Interface or Union type.
    i. Let objectType be ResolveAbstractType(fieldType, result).
    c. Let subSelectionSet be the result of calling MergeSelectionSets(fields).
    d. Return the result of evaluating ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues) normally (allowing for parallelization).

In other words, even if a field's type is an Object (and therefore has a selection set of fields that may also be resolved), if it resolves to null, no further execution happens along that path.

Upvotes: 0

Related Questions