How to throw multiple errors with express-graphql?
Asked Answered
H

1

5

In an express-graphql app, I have a userLogin resolver like so:

const userLogin = async ({ id, password }), context, info) => {

    if (!id) {
      throw new Error('No id provided.')
    }

    if (!password) {
      throw new Error('No password provided.')
    }

    // actual resolver logic here
    // … 
}

If the user doesn't provide an id AND a password, it will throw only one error.

{
  "errors": [
    {
      "message": "No id provided.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "userLogin"
      ]
    }
  ],
  "data": {
    "userLogin": null
  }
}

How is it possible to throw multiple errors in the errors response array?

Helles answered 27/9, 2018 at 7:42 Comment(3)
Maybe it's outdated but a way to achieve this is to check is it's an array then check if the elements are error or not. You'll find a topic here : github.com/graphql/graphql-js/issues/205Sp
@Sp yes, that's the same issue, but I don't get how they solved it…Discordance
I don't know graphql but i got an idea.You could trow a Json string maybe with all the errors.Pangermanism
L
10

There is no way to throw an array of errors in JavaScript or otherwise have a single resolver reject with more than one error. A GraphQL response includes an errors array and not just a single error object because the total response can include multiple errors when those errors originate from different fields. Consider this schema and resolvers:

type Query {
  a: String
  b: String
  c: String
}

const resolvers = {
  Query: {
    a: () => { throw new Error('A rejected') },
    b: () => { throw new Error('B rejected') },
    c: () => 'Still works!',
  },
}

If you query all three fields...

query { a b c }

Your data will look something like this:

{
  "errors": [
    {
      "message": "A rejected",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "a"
      ]
    },
    {
      "message": "B rejected",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ],
      "path": [
        "b"
      ]
    }
  ],
  "data": {
    "a": null,
    "b": null,
    "c": "Still works!"
  }
}

This is because GraphQL supports partial responses. However, keep in mind that this works because the fields are nullable. If they were non-null, those errors would bubble up to the closest nullable parent field.

Here are some alternative approaches:

You can utilize formatError to change how the errors returned by GraphQL are displayed to the client. That means you can include any sort of extra information with your errors, like an error code or multiple error messages. A simple example:

// The middleware
app.use('/graphql', graphqlExpress({
    schema: schema,
    formatError: (error) => ({
      message: error.message,
      path: error.path,
      locations: error.locations,
      errors: error.originalError.details
    })
}))

// The error class
class CustomError extends Error {
  constructor(detailsArray) {
    this.message = String(details)
    this.details = details
  }
}

// The resolver
const userLogin = async ({ id, password }), context, info) => {
    const errorDetails = []
    if (!id) errorDetails.push('No id provided.')
    if (!password) errorDetails.push('No password provided.')
    if (errorDetails.length) throw new CustomError(errorDetails)

    // actual resolver logic here
}

Your response then looks more like this:

{
  "errors": [
    {
      "message": "[No id provided.,No password provided.]",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "userLogin"
      ]
      "errors" [
        "No id provided.",
        "No password provided."
      ]
    }
  ],
  "data": {
    "userLogin": null
  }
}

That said, there's something a bit unsavory about returning user-facing error messages alongside GraphQL validation errors. Another approach that some APIs have taken is to include an errors field alongside the actual mutation response. For example:

type Mutation {
  userLogin: UserLoginResponse
}

type UserLoginResponse {
  response: User
  errors: [String!]
}

You can also use unions to achieve a similar effect:

type Mutation {
  userLogin: UserLoginResponse
}

type Errors {
  errors: [String!]!
}

union UserLoginResponse = User | Errors
Laryngology answered 27/9, 2018 at 11:56 Comment(2)
Thank you for your answer. I like the second the solution you suggest. That said, I have 2 questions: 1. I could use your idea to make an array of error messages first and then throw a simple error. what is the point of making a custom error here? 2. is there a way to throw multiple errors in the errors arrays from the graphQL response (my original question)?Discordance
Sorry I wasn't clear enough. As far as I'm aware, there's no way to throw multiple errors in js and no way to pass an array of them to GraphQL. The above presents a couple of workarounds for that limitation. It's not strictly necessary to create a custom error class -- you could just create a plain error and then set the details property on it.Laryngology

© 2022 - 2024 — McMap. All rights reserved.