Apollo-client: Handling promise errors from mutations client-side

Created on 15 Jun 2017  路  5Comments  路  Source: apollographql/apollo-client

I have looked everywhere for a solutions for this...

I have a mutation that is checking for run-time validation errors and throwing those back to the client. The problem I am having is my client side code does not give me back the errors array.

In the network tab in Chrome I can see the following:

{"data":{"addMount":null},"errors":[{"message":"List or Action not found.","locations":[{"line":2,"column":3}],"path":["addMount"]}]}

That is what I want. It is also coming back as a 200 response.

The console in Chrome shows:

  errors:  Error: GraphQL error: List or Action not found.
    at new ApolloError (bundle.js:10238)
    at bundle.js:32100
    at <anonymous>

What I should get back is an array of errors, but i just get an normal error. What happened to the errors array?

Client side call:

  AddMount({
      variables: {
        id: mountId,
        parent,
        parentType,
        owner,
      },
    })
    .then((data) => {
      console.log('data: ', data);
    })
    .catch((errors) => {
      console.log('errors: ', errors);
    });

Here is my mutation:

  addMount: {
      type: ActionType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) },
        parent: { type: new GraphQLNonNull(GraphQLString) },
        parentType: { type: new GraphQLNonNull(GraphQLString) },
        owner: { type: new GraphQLNonNull(GraphQLString) },
      },
      resolve(parentValue, { id, parent, parentType, owner }, req) {
        console.log('adding mount...');
        console.log('id: ', id);
        console.log('parent: ', parent);
        console.log('parentType: ', parentType);
        console.log('owner: ', owner);

        return new Promise((resolve, reject) => {

          // there can be only one mount of this id per parent
          Action.findOne({ mount: id, parent })
          .exec((err, existingMount) => {
            if (err) reject(err);

            console.log('existingMount: ', existingMount);
            if (existingMount) {
              reject('That mount already exists.');
            } else {
              let actionLookup = null;
              let listLookup = null;
              const queries = [];

              queries.push(new Promise((resolver) => {
                Action.findById(id)
                .exec((err, res) => {
                  if (err) reject(err);
                  console.log('actionLookup res: ', res);
                  actionLookup = res;
                  resolver(res);
                })
              }));
              queries.push(new Promise((resolver) => {
                List.findById(id)
                .exec((err, res) => {
                  if (err) reject(err);
                  console.log('listLookup res: ', res);
                  listLookup = res;
                  resolver(res);
                })
              }));

              // when all queries are done
              Promise.all(queries).then(() => {
                console.log('actionLookup: ', actionLookup);
                console.log('listLookup: ', listLookup);

                // must be either a list or action
                if (!listLookup && !actionLookup) {
                  reject('List or Action not found.');
                } else {
                  const type = listLookup ? 'list' : 'action';
                  console.log('type: ', type);

                  const parentModel = parentType === 'list' ? List : Action;
                  parentModel.findById(parent)
                  .exec((err, parentObj) => {
                    if (err) reject(err);

                    console.log('parentObj: ', parentObj);
                    if (!parentObj) {
                      reject('Parent was not found.');
                    } else {
                      const action = new Action({
                        mount: id,
                        type,
                        parent,
                        parentType,
                        owner,
                        order: parentObj.actions.length + 1,
                      });

                      action.save((err, action) => {
                        if (err) reject(err);

                        parentModel.findOneAndUpdate({
                          _id: parent,
                        },
                        { $push: { actions: action._id } },
                        (err, res) => {
                          err ? reject(err) : resolve(action);
                        });
                      });
                    }
                  });
                }
              });
            }
          });
        });
      },
    },

Most helpful comment

Hi @appjitsu :wave:

Apollo Client currently returns either data or an error, not both (this is because we couldn't cache errors in the normalized store in a meaningful way without a path). However, on the error you get, there should be a property that lets you access the original errors array: error.graphqlErrors. If it wasn't a GraphQL error, that property will of course not contain any errors, so make sure to watch out for that.

All 5 comments

Hi @appjitsu :wave:

Apollo Client currently returns either data or an error, not both (this is because we couldn't cache errors in the normalized store in a meaningful way without a path). However, on the error you get, there should be a property that lets you access the original errors array: error.graphqlErrors. If it wasn't a GraphQL error, that property will of course not contain any errors, so make sure to watch out for that.

Not sure why this issue was closed... please give people time to respond.

Anyhow, error.graphqlErrors is undefined. The entire error comes back as a string, not an object. Is the issue the Promise.reject(...)?

OK so I tried just throwing an error directly inside resolve:

resolve(parentValue, { id, parent, parentType, owner }, req) {
        throw new Error('an error.');
...

... and I get the same problem. What am I missing?

"apollo-client": "^1.4.2",
"express-graphql": "^0.6.6",
"graphql": "^0.10.1",
"react-apollo": "^1.4.2",

Hi, I'd also like to reject a mutation and display the rejection message in the UI.
How am I supposed to do this?
Thanks @helfer

Hi @helfer 馃憢
import React, { Component } from 'react';
import {
View, Text, TouchableOpacity,
AsyncStorage, TextInput, Keyboard
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons'
import { graphql,compose } from 'react-apollo'
import loginmutation from '../graphql/mutations/signin'
import { connect } from 'react-redux';
import { login } from '../actions/user';
import Loading from '../components/loading'
class SigninForm extends Component {

state = {
    loading:false, 
    email: '', password: '', 
}
_onChangeText = (text, type) => this.setState({ [type]: text })
_checkIfDisabled() {
    const { email, password, } = this.state;

    if (!email || !password) {
        return true;
    }

    return false;
}
_onSignupPress = async () => {
    this.setState({ loading: true });

    const { email, password } = this.state;
    //const avatar = 'http://i65.tinypic.com/mrb979.png';

    try {
        const { data } = await this.props.mutate({
            variables: {
                email,
                password,
                //avatar,
            },
        });
        await AsyncStorage.setItem(
        '@twitteryoutubeclone', data.login.token);
         this.setState({ loading: false });
        return this.props.login();
    } catch (error) {
        throw error;
    }
};

render() {
    if (this.state.loading) {
        return <Loading />;
    }
    return (
        <View
            onPress={() => Keyboard.dismiss()}
            style={{
                flex: 1, justifyContent: 'center',
                alignItems: 'center',
                backgroundColor: '#353b48', position: 'relative'
            }}>
            <TouchableOpacity
                hitSlop={{ top: 20, bottom: 20, right: 20, left: 20 }}
                style={{
                    zIndex: 1,
                    justifyContent: 'center',
                    alignItems: 'center', position: 'absolute',
                    top: '5%', left: '5%'
                }} onPress={this.props.onBackPress}>
                <Icon color='white' size={30} name="arrow-back" />
            </TouchableOpacity>
            <View style={{
                flex: 1,
                alignSelf: 'stretch', alignItems: 'center',
                justifyContent: 'center',
            }}>                    
                <View style={{
                    height: 50, width: '70%', borderBottomWidth: 2,
                    borderBottomColor: 'white', marginVertical: 5
                }}>
                    <TextInput style={{
                        height: 50, fontSize: 30,
                        color: 'white'
                    }}
                        placeholder="Email" keyboardType="email-address"
                        autoCapitalize="none"
                        onChangeText={text => this._onChangeText(text, 'email')}
                    />
                </View>
                <View style={{ paddingBottom: 40, }}></View>
                <View style={{
                    height: 50, width: '70%', borderBottomWidth: 2,
                    borderBottomColor: 'white', marginVertical: 5
                }}>
                    <TextInput style={{
                        height: 50, fontSize: 30,
                        color: 'white'
                    }}
                        placeholder="Password" secureTextEntry
                        onChangeText={text => this._onChangeText(text, 'password')}
                    />
                </View>
                <View style={{ paddingBottom: 160 }}></View>

            </View>
            <TouchableOpacity
                onPress={this._onSignupPress}
                disabled={this._checkIfDisabled()}
                style={{
                    justifyContent: 'center',
                    alignItems: 'center', position: 'absolute', bottom: '15%',
                    width: '70%', height: '10%', backgroundColor: '#0097e6',
                    borderRadius: 10, shadowOpacity: 0.2, shadowRadius: 5,
                    shadowOffset: '0px 2px', shadowColor: '#000', elevation: 2
                }} >
                <Text style={{
                    fontSize: 30,
                    borderRadius: 10, justifyContent: 'center',
                    alignItems: 'center', color: 'white', fontWeight: '900'
                }}>Login</Text>
            </TouchableOpacity>
        </View>
    );
}

}

export default compose(graphql(loginmutation),
connect(undefined, { login }))(
SigninForm,

);

Possible Unhandled Promise Rejection (id: 0):
Error: GraphQL error: User not exist!
at new ApolloError

Was this page helpful?
0 / 5 - 0 ratings