1
votes

For a mutation addVoucher there are a limited list of potential errors that can occur.

  • Voucher code invalid
  • Voucher has expired
  • Voucher has already been redeemed

At the moment I'm throwing a custom error when one of these occurs.

// On the server:
const addVoucherResolver = () => {
    if(checkIfInvalid) {
        throw new Error('Voucher code invalid')
    }
    return {
        // data
    }
}

Then on the client I search the message description so I can alert the user. However this feels brittle and also the GraphQL API doesn't automatically document the potential errors. Is there a way to define the potential errors in the GraphQL schema?

Currently my schema looks like this:

type Mutation {
    addVoucherResolver(id: ID!): Order
}

type Order {
    cost: Int!
}

It would be nice to be able to do something like this:

type Mutation {
    addVoucherResolver(id: ID!): Order || VoucherError
}

type Order {
    cost: Int!
}

enum ErrorType {
    INVALID
    EXPIRED
    REDEEMED
}

type VoucherError {
    status: ErrorType!
}

Then anyone consuming the API would know all the potential errors. This feels like a standard requirement to me but from reading up there doesn't seem to be a standardises GraphQL approach.

2

2 Answers

1
votes

It's possible to use a Union or Interface to do what you're trying to accomplish:

type Mutation {
  addVoucher(id: ID!): AddVoucherPayload
}

union AddVoucherPayload = Order | VoucherError

You're right that there isn't a standardized way to handle user-visible errors. With certain implementations, like apollo-server, it is possible to expose additional properties on the errors returned in the response, as described here. This does make parsing the errors easier, but is still not ideal.

A "Payload" pattern has emerged fairly recently for handling these errors as part of the schema. You see can see it in public API's like Shopify's. Instead of a Union like in the example above, we just utilize an Object Type:

type Mutation {
  addVoucher(id: ID!): AddVoucherPayload
  otherMutation: OtherMutationPayload
}

type AddVoucherPayload {
  order: Order
  errors: [Error!]!
}

type OtherMutationPayload {
  something: Something
  errors: [Error!]!
}

type Error {
  message: String!
  code: ErrorCode! # or a String if you like
}

enum ErrorCode {
  INVALID_VOUCHER
  EXPIRED_VOUCHER
  REDEEMED_VOUCHER
  # etc
}

Some implementations add a status or success field as well, although I find that making the actual data field (order is our example) nullable and then returning null when the mutation fails is also sufficient. We can even take this one step further and add an interface to help ensure consistency across our payload types:

interface Payload {
  errors: [Error!]!
}

Of course, if you want to be more granular and distinguish between different types of errors to better document which mutation can return what set of errors, you won't be able to use an interface.

I've had success with this sort of approach, as it not only documents possible errors, but also makes it easier for clients to deal with them. It also means that any other errors that are returned with a response should serve as an immediately red flag that something has gone wrong with either the client or the server. YMMV.

0
votes

You can use scalar type present in graphql just write scalar JSON and return any JSON type where you want to return it.

`
  scalar JSON
  type Response {
    status: Boolean
    message: String
    data: [JSON]
  }
`

Here is Mutation which return Response

  `
  type Mutation {
    addVoucherResolver(id: ID!): Response
  }
`

You can return from resolver

return {
      status: false,
      message: 'Voucher code invalid(or any error based on condition)',
      data: null
  }

or

return {
      status: true,
      message: 'Order fetch successfully.',
      data: [{
          object of order
      }]
  }

on Front end you can use status key to identify response is fetch or error occurs.