relay: GraphQL error handling in Relay Modern

I think error handling is a little bit weird.

Contra the example in https://facebook.github.io/relay/docs/network-layer.html, it looks like I need to actually throw an Error in the event that there are any query errors. I can’t just pass through the graphql object with the errors property.

This is because in https://github.com/facebook/relay/blob/v1.1.0/packages/relay-runtime/network/RelayNetwork.js#L197-L208, normalizePayload only throws an error if data is nully.

But the forward throw branch there is the only code path that will trigger onError for the request, and correspondingly the only path that will hit onError in <QueryRenderer> per https://github.com/facebook/relay/blob/v1.1.0/packages/react-relay/modern/ReactRelayQueryRenderer.js#L205-L212.

Otherwise we hit onNext per https://github.com/facebook/relay/blob/v1.1.0/packages/react-relay/modern/ReactRelayQueryRenderer.js#L229, and will always set error: null on the readyState that gets passed to the render callback.

This is weird. Am I missing something, or are things supposed to work this way?

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 61
  • Comments: 39 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Seems like there’s a lot of confusion on what @taion is talking about in this thread.

I think I’m hitting the same issue @taion is and wanted to illustrate with some examples. Basically, I’m passing GraphQL errors back to Relay and they’re getting swallowed by QueryRenderer. Something like:

{
  data: {
    viewer: {
      me: null
    }
  },
  errors: [{message: 'Bad error', code: 'some error' ...}]
}

Then in the render function of QueryRenderer I get props like { viewer: {me: null }} and error is null. However, I am expecting to get back the GraphQL errors I passed back.

For the nully data returning a processed version of the error that @taion mentions, if I return:

{
  data: null,
  errors: [{message: 'Bad Error', code: 'some error' ...}]
}

from the server, then I get my error to come through but it gets formatted and aggregated into some Relay Modern error:

{
  name: "RelayNetwork", 
  type: "mustfix",
  framesToPop: 2,
  source: {
    errors: [ Original GraphQL Errors are here ],
    operations: {...},
    variables: {}
  },
  ...
}

So it seems like I can access my errors if I force my data to be nully as @taion mentioned but that definitely feels a bit weird. Also, the GraphQL spec says errors can be returned even when some data can be returned. In their example, one of the elements is missing a name property and an error is returned and name is set to null, but the rest of the graph is returned. http://facebook.github.io/graphql/#sec-Errors

Not quite the issue – the problem is actually in QueryRenderer.

For me setting data to null was not an option, because I needed the partial data and handle the errors somewhere in my components as well. I found a workaround to be able to get partial errors inside my query payload in Relay containers. I basically wanted to be able to create a query like following

query {
  errors {
    message
    path
  }
  someOtherField {
    ...
  }
}

This way the partial errors are accesible inside my components. To accomplish this I created a new error type in the GraphQL schema. I use graphql-js here.

import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLList } from 'graphql';
import { globalIdField } from 'graphql-relay';

const errorLocationType = new GraphQLObjectType({
  name: 'ErrorLocation',
  fields: () => ({
    line: { type: GraphQLInt },
    column: { type: GraphQLInt },
  }),
});

const errorType = new GraphQLObjectType({
  name: 'Error',
  description: 'Error type. Not used on server but only on client as a workaround to get access to server errors in client components',
  fields: () => ({
    id: globalIdField('Error'),
    message: {
      type: GraphQLString,
      description: 'The errors message',
    },
    locations: {
      type: new GraphQLList(errorLocationType),
      description: 'The errors locations (probably meaning inside the query)',
    },
    path: {
      type: new GraphQLList(GraphQLString),
      description: 'The errors path (for an error in field "restrictedField" in { catalog: { unit: { restrictedField } } the path would be ["catalog", "unit", "restricedField"]',
    },
});

export default errorType;

I added an errors field on the root query type.

  ...
  fields: () => ({
    ...
    errors: {
      type: new GraphQLList(errorType),
      description: 'This is a list of server errors occurring during a query. It won\'t be set on the server response but inside the fetcher',
    },
    ...
  })
  ...

The errors field does not have a resolver. So errors is always null in the GraphQL response payload. The trick is to fill the errors inside the fetcher as follows.

async fetch(operation, variables) {
  const response = await fetch(this.url, {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      ...this.headers,
    },
    body: JSON.stringify({ query: operation.text, variables }),
  });

  const json = await response.json();
  if (json && json.errors && json.data) {
    json.data = Object.assign({}, json.data, { errors: json.errors});
  }
  return json;
}

You can query the errors field now in any component. For testing you can add a type like following to your schema, where one field cannot be resolved due to some error.

import { GraphQLObjectType, GraphQLString } from 'graphql';
import { globalIdField } from 'graphql-relay';
import { UserError } from 'graphql-errors';

import errorType from './errorType';

const authorizedTestType = new GraphQLObjectType({
  name: 'AuthorizedTest',
  description: 'Type to test authorized fields',
  fields: () => ({
    id: globalIdField('AuthorizedTest'),
    public: {
      type: GraphQLString,
      resolve: () => 'This is public',
    },
    private: {
      type: GraphQLString,
      resolve: () => Promise.reject(new UserError('401')),
    },
    error: {
      type: errorType,
    },
  }),
});

export default authorizedTestType;

If you want to handle server errors directly in your component you can add an error field to the corresponding type as above. We can use the fetcher again to set the error on this type. Following function is not well tested, but does the job for now.

export const addErrorsToData = (originalData, errors) => {
  const data = { ...originalData, errors };
  errors.forEach((error) => {
    const pathUntilToErrorableObject = error.path.slice(0, -1);
    return pathUntilToErrorableObject.reduce(
      (dataChild, childKey, index) => {
        if (index < pathUntilToErrorableObject.length - 1) {
          return dataChild[childKey];
        }

        // eslint-disable-next-line no-param-reassign
        dataChild[childKey].error = {
          ...error,
          field: error.path[error.path.length - 1],
        };
        return dataChild;
      },
      data,
    );
  });
  return data;
};

You can call it in the fetchers fetch function like following

if (json && json.errors && json.data) {
  json.data = addErrorsToData(json.data, json.errors);
}

Still trying to figure out how to handle “network is unavailable” in Relay without it crashing in production - I have the error in fetchQuery but don’t know what to do to get it back to the Component to display. Is everyone here stuck on the same thing or can someone point me to a resource?

I have somewhat older version of Relay Modern (v.1.6.0). This is how I fixed the issue.

When graphql sends error message, it has a default format like following,

{
  data: null,
  errors: [{message: 'Bad Error', code: 'some error' ...}]
}

You can modify it to simple format by adding formatError image (for more info on how to add custom error message, go here: https://medium.com/@estrada9166/return-custom-errors-with-status-code-on-graphql-45fca360852)

After doing this, Now graphql will send error message in following format. Now errors is array of string.

{
  data: null,
  errors: ['Bad Error']
}

Now go to your front-end side code, open environment.js (file in which relay environment is setup)
add these lines to returned fetch function of fetchQuery function.

 .then(json => {
      if (json && json.errors) {
        throw new Error('Server Error: ' + json.errors[0])
      }
      return json;
    });

Now, It will kind of look like this:

function fetchQuery(
  operation,
  variables,
) {
  return fetch('/graphql', {
    method: 'POST',
    headers: {
      'content-type': 'application/json'
    },
    body: JSON.stringify({
      query: operation.text, 
      variables,
    }),
  }).then(response => {
    return response.json();
  }).then(json => {
    if (json && json.errors) {
      throw new Error('Server Error: ' + json.errors[0])
    }
    return json;
  })
};

Note:- Don’t catch this error. This thrown error will get transmitted to QueryRenderer where it will be available as error prop. handle it there (show error related UI if error is present).

<QueryRenderer
        environment={environment}
        query={query}
        render={({error, props}) => {
          if (error) {
            return <div>{error.message}</div>;  //render UI based on error
          } else if (props) {
            return <div>{props.page.name} is great!</div>;
          }
          return <div>Loading</div>;
        }}
/>

Please fix this, this is annoying!

@renatonmendes Thank you for your answer. This is my code:

  return fetch(
    '/api/graphql',
    {
      method: 'POST',
      body: JSON.stringify({
        query: operation.text,
        variables,
      }),
    },
  ).then(response => response.json()).then(responeJson => {
    if (responeJson.errors) {
      return {
        data: null,
        errors: responeJson.errors
      }
    }
    console.log(responeJson)
    return responeJson
  });
}  

But i have on error on TypeError: errors.map is not a function (like if errors would be an array and not object, but if i put that object in array is not a very clean solution)

Yup, I love that post. It has a great breakdown of these problems. “very manual process” definitely was meant to encompass the work involved around setting up all the types/interfaces/unions required to represent these errors, but even after you’ve implemented most of the pieces in that blog post, without a lot of additional work, in most backend frameworks I’ve seen, its still relatively easy to end up with errors in your root level errors object. If you follow the advice in that blog post and have confidence most errors will come through as typed data in your response, you can always use one of the strategies mentioned above to bubble any remaining errors up to the root and have it fail at the query level. Every framework I’ve worked with (but I haven’t looked at too many yet) default to having errors in a resolver will automatically fall back to making the closest nullable parent field null and inserting the error into the root errors list.

For most applications I’ve worked in, developers try to handle the obvious error cases like a failed request, but there are almost always cases that are not explicitly handled, and explicitly handling every possible error case is often not practical.

I am not trying to suggest that using unstructured errors like this is a good pattern, just that is a pattern which is common, and even in backends that try to implement better error handling by baking it into the schema are still likely to have some errors fall through the cracks and fall back into the root errors array. In these cases, I would personally prefer to have a way to extract those errors when retrieving the data/fields which the errors correspond to, rather than being forced to handle them at the root level.

I have been wondering if there would be a way to add support for additional patterns around error handling by adding a few new APIs.

At a high level, the issue seems to be that relay does not have a way to represent non-critical errors, or errors which are not intended to bubble all the way to the root of the query without introducing/representing those errors at the schema level. While I understand the value and benefits of properly representing error states you want to handle in your schema, and treating them as data rather than exceptions, many backend frameworks and patterns have been built around the idea of partial responses, and allowing errors in nullable fields to automatically be captured in the root error array with a path, and letting the rest of the response to succeed. In most of the backend frameworks for graphql I’ve looked at, automatically creating intermediate objects or unions for representing various error states is a very manual process, and I have not seen good examples of patterns that make handing things like failed network requests to an external service easy to handle without a lot of manual error handling and custom types.

I think eventually there will be some new backend patterns that will make this easier, but the current state of the world seems to be that there are lots of real world examples of graphql schemas that use patterns with partial + errors in responses.

Here is my ideal solution:

  • Add a new representation of errors into the relay store, so that when a graphql response is received it can also provide a set of (non critical) errors to keep in the store.
  • Add a new way of resolving fields from RecordProxies, either by adding a new method, or adding an option to getValue that looks for errors associated with the field being retrieved and can return/throw those errors.
  • Create a new set of components/hooks or update the existing components/hooks for fragments/connections/queries that will throw when accessing/unmasking the data for the fields defined in those fragments.

The end result of this would be that you could create error boundaries around components that consume a fragment and handle errors for more granular parts of a query without having to break down your query into smaller requests to get more graceful degradation.

I am pretty new to relay, so my understanding of relays concepts are pretty rough, but I think adding something like this could be done in a way that adds minimal overhead to existing patterns and would create something that is completely opt-in, while still providing a solution to a large set of problems around error handling.

if you really want to handle them gracefully, then you can also effectively (mostly) ignore request-level errors, and just blow up on null dereference in the relevant components

I think the issue there is that it assumes that a null means there was an error, which is often not the case, and would effectively mean you your schema can either have valid nulls or errors, but not both.

It seems like you are suggesting that there is are patterns you can use in your schema/server to represent any error condition in you schema. I don’t disagree, but I also don’t think it is a cleaner solution. Having every field wrapped in a union makes your queries and you schema a lot more complicated, many schemas already exist, and use the simpler pattern of nulls + top level errors, and migrating these schemas to properly represent all their error cases in the schema might be a lot more complicated than exposing a way to access these errors in relay.

I was not trying to advocate for this pattern to be the preferred way to handle errors in relay, just hoping it would be possible to expose these errors for users who would like to be able to access them somewhere outside of the root level query or the network layer.

But, after thinking about the problem a little more, I think all this is actually pretty manageable without anything in relay itself. It should be pretty straight forward to basically map errors to object ids + fields at the network layer, then write some hooks that wrap the relay hooks to check for errors when unmasking a fragment.

I decided to go other way - I dont think commitMutation should reject at all (because “partial success” feature is one of the founding principles of GraphQL)

import { commitMutation } from 'react-relay'

// inspired by https://github.com/relay-tools/relay-commit-mutation-promise
// check https://github.com/facebook/relay/issues/1913 for more info

export default function commitMutationPromise(environment, config) {
  return new Promise(function(resolve, _reject) {
    commitMutation(
      environment,
      Object.assign(
        {},
        config,
        {
          // onCompleted is called when:
          // - data is not null (it doesn't matter if errors field is empty or not)
          onCompleted: function(data, errors) {
            // restore original response
            resolve({ data, errors })
          },

          // onError is called when:
          // - data is null
          // - errors array is not empty
          onError: function (errors) {
            // TODO: can errors be an js error object? reject then?

            // never reject
            // (reject is pointless because "partial success" feature is one of the founding principles of GraphQL),

            // restore original response
            resolve({ data: null, errors })
          },
        }
      )
    )
  })
}

Leaving a link to @josephsavona’s explanation here for posterity.