Why am I getting a "Cannot return null for non-nullable field" error when doing a mutation?

Collins Orlando picture Collins Orlando · Apr 25, 2018 · Viewed 34.7k times · Source

I'm trying my hand at (Apollo) GraphQL on the server side and have been having a probably silly issue. I'm trying to sign up a user, but keep getting the error shown in the linked image below. What is the problem? Ignore the very simple auth flow, as I'm just testing out the GraphQl

enter image description here

Here are the relevant code snippets:

Schema

export default `

type User {
    id: ID!
    name: String!
    email: String!
}

type Query {
    allUsers: [User]
  currentUser: User
}

type Mutation {
    createAccount(name: String!, email: String!, password: String!): User
    loginUser(email: String!, password: String!): User
    updatePassword(email: String!, password: String!, newPassword: String!): User
    deleteAccount(email: String!, password: String!): User
}

`

Resolvers

createAccount: async (
  parent,
  { name, email, password },
  { User },
  info
) => {
  try {
    // Check for invalid (undefined) credentials
    if (!name || !email || !password) {
      return 'Please provide valid credentials';
    }

    // Check if there is a user with the same email
    const foundUser = await User.findOne({ email });

    if (foundUser) {
      return 'Email is already in use';
    }

    // If no user with email create a new user
    const hashedPassword = await bcrypt.hash(password, 10);
    await User.insert({ name, email, password: hashedPassword });

    const savedUser = await User.findOne({ email });

    return savedUser;
  } catch (error) {
    return error.message;
  }
},

Answer

Daniel Rearden picture Daniel Rearden · Apr 25, 2018

The biggest problem with your resolver is that in any number of scenarios, instead of returning a User object, you return a string. Your schema specifies that createAccount can return a User or null (if it was User!, it would be non-nullable and then null would not be a valid type either).

When you return a string in your resolver, because it's expecting an object, it coerces it into one and then starts looking for User properties on that object (like name and email). Those properties don't exist, and because they are non-null properties on your User object, returning null/undefined for them results in an error.

Your resolver should probably just throw whatever errors it needs to throw. Then they will be returned as part of the errors array in your response. For example:

// Check if there is a user with the same email
const foundUser = await User.findOne({ email })

if (foundUser) throw new Error('Email is already in use')

// If no user with email create a new user
const hashedPassword = await bcrypt.hash(password, 10);
await User.insert({ name, email, password: hashedPassword });

const savedUser = await User.findOne({ email });

return savedUser;

Now, if you have a duplicate email, the response will look something like this:

{
  "data": {
    "createAccount": null
  },
  "errors": [
    {
      "message": "Email is already in use",
      "locations": [
        {
          "line": 4,
          "column": 3
        }
      ],
      "path": [
        "createAccount"
      ]
    }
  ]
}

If you want to manipulate how your errors are shown on the client, you should utilize the formatError or formatResponse configuration options for your Apollo server middleware. It's also good practice to use custom errors, allowing you to add custom properties like code to more easily identify the error type on the client side.

Lastly, it's unnecessary to check if name, email or password are defined inside your resolver -- your schema already has these inputs marked as non-null, which means GraphQL will automatically return an error if any of them are missing.