MichalLytek / type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
https://typegraphql.com
MIT License
7.98k stars 674 forks source link

When multiple resolvers throw an error, the `errors` array in the response only contains one error #1661

Closed bageren closed 2 months ago

bageren commented 3 months ago

Describe the Bug When multiple resolvers throw an error, the errors array in the response only contains one error.

To Reproduce

  1. Create a resolver with 2 field resolvers inside.
  2. Throw an error inside both field resolvers
  3. Start up the graphql server (I used @apollo/server@4.7.1)
  4. Send a query to the server that triggers both field resolvers

Something like this:

@ObjectType()
class Recipe {
  @Field(() => ID)
  id: string | undefined;

  @Field()
  title: string | undefined;
}

@Resolver(() => Recipe)
export class RecipeResolver {
  @Query(() => Recipe)
  async recipe() {
    return {};
  }

  @FieldResolver()
  async id(): Promise<any> {
    throw new Error("id error");
  }

  @FieldResolver()
  async title(): Promise<any[]> {
    throw new Error("title error");
  }
}

Expected Behavior The errors array in the response should include both errors, something like:

{ "data": {}, "errors": [ { "message": "id error", }, { "message": "title error", } ] }

Logs

Environment (please complete the following information):

Additional Context

MichalLytek commented 3 months ago

Can you reproduce this with using pure graphql-js lib?

bageren commented 3 months ago

Can you reproduce this with using pure graphql-js lib?

Yes

import "reflect-metadata";
import { graphql } from "graphql";
import { buildSchemaSync } from "type-graphql";
import { RecipeResolver } from "./recipe_resolver";

const schema = buildSchemaSync({
  resolvers: [RecipeResolver],
});

graphql({
  schema,
  source: `query ExampleQuery {
  recipe {
    id
    title
  }
}`,
}).then((d) => console.log(d.errors.length));
// logs 1
MichalLytek commented 3 months ago

I mean try to build the types and resolvers with graphql-js constructs, without TypeGraphQL.

bageren commented 3 months ago

I mean try to build the types and resolvers with graphql-js constructs, without TypeGraphQL.

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: "RootQueryType",
    fields: {
      recipe: {
        type: new GraphQLObjectType({
          name: "Recipe",
          fields: {
            id: {
              type: GraphQLString,
              resolve: () => {
                throw new Error("id error");
              },
            },
            title: {
              type: GraphQLString,
              resolve: () => {
                throw new Error("title error");
              },
            },
          },
        }),
        resolve() {
          return {};
        },
      },
    },
  }),
});

graphql({
  schema,
  source: `query ExampleQuery {
  recipe {
    id
    title
  }
}`,
}).then((d) => console.log(d.errors.length));
// logs 2
MichalLytek commented 2 months ago

Closing as works as intended 🔒

Explanation: GraphQL runtime is designed to "swallow" errors - bubble it up, until it reaches nullable type. Then it places null for that part of response data tree and continue execution for other fields. Then it returns data and all the errors captured and "swallowed" during execution. When there's no nullable field, it returns data: null and only the current error. Why? Because the rest of the field resolver could be not even called yet. To see that behavior, all you need is to modify your graphql-js example and use GraphQLNonNull which is implicit in TypeGraphQL:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: "RootQueryType",
    fields: {
      recipe: {
        type: new GraphQLObjectType({
          name: "Recipe",
          fields: {
            id: {
              type: new GraphQLNonNull(GraphQLString),
              resolve: async () => {
                throw new Error("id error");
              },
            },
            title: {
              type: new GraphQLNonNull(GraphQLString),
              resolve: async () => {
                throw new Error("title error");
              },
            },
          },
        }),
        async resolve() {
          return {};
        },
      },
    },
  }),
});

Then the response does not contain both errors:

image