Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to throw multiple errors with express-graphql?

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?

like image 393
François Romain Avatar asked Jan 01 '23 19:01

François Romain


1 Answers

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
like image 107
Daniel Rearden Avatar answered Jan 05 '23 02:01

Daniel Rearden