Managing GraphQL error codes and sending them from server to client in Node.js app

I’m working on a project with React frontend and Node.js backend using GraphQL. I need help with error handling in GraphQL operations. When my GraphQL queries fail, I want to send proper HTTP status codes (like 400 for bad requests, 409 for conflicts, etc.) from my server to the client side. The problem is that GraphQL always returns 200 OK even when there are errors in the response.

Server Setup (Node.js + GraphQL)

I’m using express-graphql for my GraphQL endpoint and trying to handle errors with a custom formatter:

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { GraphQLError } = require('graphql');
const mongoose = require('mongoose');
const cors = require('cors');
const { buildSchema } = require('graphql');

const server = express();
const DATABASE_URL = process.env.DATABASE_CONNECTION;
const SERVER_PORT = 3000;

server.use(cors());
server.use(express.json());

server.use('/api/graphql', graphqlHTTP({
  schema: mySchema,
  rootValue: myResolvers,
  graphiql: true,
  formatError: (error) => {
    console.log('Handling error:', error);
    
    if (error instanceof GraphQLError) {
      return new GraphQLError(error.message, {
        originalError: error,
        extensions: {
          code: error.extensions?.code || 400,
          details: error.extensions?.details || {},
        },
      });
    }

    return new GraphQLError('Something went wrong on server!', {
      originalError: error,
      extensions: { code: 500, details: {} },
    });
  },
}));

server.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const errorMessage = err.message;
  const errorData = err.data;
  res.status(statusCode).json({ message: errorMessage, data: errorData });
});

const initializeServer = async () => {
  try {
    await mongoose.connect(DATABASE_URL);
    server.listen(SERVER_PORT, () => {
      console.log(`GraphQL server running on port ${SERVER_PORT}`);
    });
  } catch (error) {
    console.log('Server initialization failed:', error);
  }
};

initializeServer();

Client Side (React)

On the frontend, I’m making GraphQL requests but can’t get the proper status codes:

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const RegistrationForm = () => {
  const [loading, setLoading] = useState(false);
  const [errorMsg, setErrorMsg] = useState(null);
  const navigate = useNavigate();

  const handleUserRegistration = async (formData) => {
    setLoading(true);
    
    const mutation = {
      query: `
        mutation RegisterUser($input: UserInput!) {
          registerUser(userInput: $input) {
            id
            username
            email
          }
        }
      `,
      variables: {
        input: {
          username: formData.username,
          email: formData.email,
          password: formData.password
        }
      }
    };

    try {
      const response = await fetch('http://localhost:3000/api/graphql', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(mutation),
      });

      console.log('Response status:', response.status); // Always shows 200

      const result = await response.json();
      console.log('Server response:', result);
      
      if (result.errors && result.errors.length > 0) {
        const firstError = result.errors[0];
        const errorCode = firstError.extensions?.code;
        const errorDetails = firstError.extensions?.details;
        const message = firstError.message;

        if (errorCode === 409) {
          setErrorMsg('This email is already registered!');
        } else if (errorCode === 422) {
          setErrorMsg('Please check your input data!');
        } else {
          setErrorMsg(message || 'Registration failed!');
        }
        
        setLoading(false);
        return;
      }

      setLoading(false);
      navigate('/dashboard');
      
    } catch (error) {
      console.error('Network error:', error);
      setErrorMsg('Network error occurred!');
      setLoading(false);
    }
  };

  return (
    <div>
      {/* Registration form JSX here */}
    </div>
  );
};

export default RegistrationForm;

The Issue:

My backend creates errors with custom status codes in the extensions, but the HTTP response always comes back as 200 OK. The frontend never sees status codes like 409 or 422 in the actual HTTP response. I can access the error codes through the GraphQL error extensions, but I want to know if there’s a way to make the HTTP status reflect the actual error type.

Is there a standard approach for handling this with GraphQL and Express? Should I be setting the response status differently, or is working with error extensions the right way to go?

interesting problem! you could write custom middleware to intercept the GraphQL response and modify the HTTP status based on error extensions before it goes out. I’ve seen devs do this with express.

why do you want HTTP status codes instead of handling error extensions? is it for monitoring/logging or client-side error handling? what’s your specific use case?

graphql spec basically ignores http status codes. most devs stick with the extensions approach you’re using. you could set res.status() in your resolver before throwing the error if you really need status codes, but it’s hacky and breaks graphql conventions.

That’s actually how GraphQL works by design. The spec separates HTTP transport from GraphQL errors, so you’ll always get 200 OK even when there are application errors. I’ve hit this same issue in production. The cleanest fix I found was custom middleware that checks the GraphQL response before sending it to the client. You examine the response for errors with specific extension codes, then modify the HTTP status accordingly. Here’s what worked for me: create middleware that runs after your GraphQL handler, parse the response body to detect error extensions, then set res.status() based on your error codes. Just heads up though - this breaks GraphQL conventions and might mess with some GraphQL clients that expect 200 responses. For most apps, sticking with error extensions like you’re doing now is the way to go. Better compatibility with GraphQL tooling and follows established patterns.