Graphql cannot return null for non nullable field errors

Graphql cannot return null for non nullable field errors

You’re hitting the graphql cannot return null for non-nullable field error because your schema promised the client a value — and your resolver broke that promise. GraphQL treats this as a hard contract violation: a field marked String! or ID! must always return a real value, never null. When a resolver returns null for such a field, GraphQL doesn’t silently skip it — it nulls out the parent object, and that null can cascade all the way up to the root query, returning nothing at all.

This isn’t a bug in GraphQL — it’s the type system doing exactly what it was designed to do. But it does mean you need to know where the null is coming from and whether to fix the resolver or relax the schema. This guide walks you through both.

What You’ll Learn

  • Why this error happens — and how GraphQL’s null propagation works under the hood.
  • How to read the error path to pinpoint the exact failing resolver in seconds.
  • Concrete resolver fixes — with before/after code for JavaScript, TypeScript, Elixir, and Java.
  • When to fix the resolver vs. change the schema — and how to make that call without breaking clients.
  • Framework-specific solutions for Apollo Server, Absinthe, TypeORM/type-graphql, and others.
  • Production case studies — how real teams diagnosed and resolved this under pressure.

Understanding the Cannot Return Null for Non-Nullable Field Error

When GraphQL executes a query, it runs your resolvers and then validates every returned value against the schema. If a field is declared non-nullable with ! and its resolver returns null, the validation step immediately fails — but not just for that field. GraphQL propagates the null upward through the response tree until it hits a nullable field or the root. This means a single null in a deeply nested resolver can silently wipe out large chunks of your response.

Here’s what the actual error looks like in the response JSON:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable field User.name",
      "locations": [{ "line": 3, "column": 5 }],
      "path": ["user", "name"],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ]
}

Notice that data.user is null — not just data.user.name. Because name was non-nullable, GraphQL couldn’t leave the User object in a partially-valid state, so it nulled the whole parent. If the query had fetched users: [User!]!, the entire list would be gone.

  • Non-nullable fields are marked with ! in the schema — e.g., name: String!
  • A null return from any resolver for a ! field triggers this error
  • GraphQL propagates the null upward to the nearest nullable ancestor
  • The path array in the error response tells you exactly where the null occurred
  • This behavior is defined in the GraphQL specification, not framework-specific

The Technical Explanation Behind the Error

The ! syntax in GraphQL schema definitions creates a non-nullable constraint enforced at execution time. When you write name: String!, you’re telling both clients and the server runtime: this field will always have a string value.

type User {
  id: ID!           # Non-nullable — always required
  name: String!     # Non-nullable — must have a value
  email: String     # Nullable — can be null
  profile: Profile  # Nullable — can be null
}

The GraphQL execution engine validates resolved values after all resolvers have run, before the response is sent. The validation is strict and implementation-agnostic — it works the same way in Apollo Server, graphql-java, Absinthe, and every other spec-compliant server. Your resolver code is responsible for ensuring it never returns null (or undefined in JavaScript) for a non-nullable field under any circumstances.

This error is a specific form of a GraphQL validation error — it means the runtime value violated a constraint declared in your schema. Unlike schema validation errors (which catch malformed queries), this one fires at execution time, after resolvers run.

Common Scenarios That Trigger This Error

In practice, this error almost always comes from one of five places:

  • Database query returns no row — e.g., a user ID that no longer exists, or a soft-deleted record
  • Missing foreign key relationship — the related entity was deleted but the FK reference remains
  • External API returns null or fails — network errors, rate limiting, or a 404 from a downstream service
  • Unhandled async error in resolver — a thrown error that isn’t caught, causing the resolver to return undefined
  • Schema vs. data mismatch after migration — fields marked ! that contain null values in legacy rows

The most common pattern in async JavaScript resolvers is the silent undefined. An awaited function that throws but isn’t wrapped in try/catch will cause the resolver to return undefined, which GraphQL treats the same as null for non-nullable fields:

// ❌ Dangerous — if getUser throws, resolver returns undefined
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return await db.getUser(id); // throws if not found
    }
  }
};

// ✅ Safe — explicit handling
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await db.getUser(id);
      if (!user) throw new GraphQLError(`User ${id} not found`, {
        extensions: { code: 'NOT_FOUND' }
      });
      return user;
    }
  }
};

The null propagation behavior is intentional in the spec — it prevents clients from receiving silently broken partial data. But it means your resolvers need to be explicit: either return a valid value, or throw a real error.

GraphQL Error Structure Analysis

Before you can fix the error, you need to read it correctly. The error response always contains a path array — this is your most important debugging tool.

Error ComponentWhat It Tells YouExample
pathExact field location in the query tree["user", "profile", "name"]
messageWhich field violated the constraintCannot return null for non-nullable field User.name
locationsLine/column in the query document[{"line": 3, "column": 5}]
extensions.codeMachine-readable error categoryINTERNAL_SERVER_ERROR

Read the path array left to right — each element is a deeper level in your query. ["user", "profile", "name"] means: inside the user query → inside the profile field → the name field returned null. That immediately tells you to look at the resolver for Profile.name, not the root user resolver.

“message”: “Cannot return null for non-nullable field UpdateItemPayload.item.”
Parse Platform Community
Source link

Diagnosing the Root Cause in Your Implementation

The fastest way to diagnose this error is to follow the path directly to the resolver. Here’s the workflow that works in production:

  1. Read the path array in the error — identify the exact type and field (e.g., User.name)
  2. Find the resolver for that field in your codebase — check both the type resolver and any parent resolver that might be short-circuiting
  3. Add a temporary console.log or breakpoint immediately before the return — log the value being returned
  4. Check the data source: run the database query or API call in isolation to confirm it returns null
  5. Decide: is null a legitimate state for this data? If yes → make the field nullable. If no → fix the data source or add a fallback.
  6. Remove debug logging, add proper error handling or default value, test with the original query

The critical step most developers skip is step 4 — running the query in isolation. Don’t assume the null comes from the resolver logic itself. Often it’s the database returning zero rows for a valid reason (deleted record, permission filter, etc.).

Examining Your Schema Definitions

After identifying the failing field, look at its schema definition. Ask one question: Is this field truly guaranteed to have a value in 100% of cases? If the honest answer is “mostly yes, but sometimes no,” it should be nullable.

Problematic PatternImproved PatternWhy
type User { name: String! }type User { name: String }Users from OAuth may not have a display name set
type Post { author: User! }type Post { author: User }Author accounts may be deleted; post still exists
type Product { price: Float! }type Product { price: Float! } + resolver defaultPrice must exist, but resolver should provide fallback

A good rule of thumb from production experience: apply ! to IDs and system-generated values (timestamps, auto-increment keys) where the database guarantees non-null. Be more cautious with user-provided or relationship fields. You can always make a nullable field non-nullable later (additive, non-breaking) — but going the other direction requires a major version bump or migration.

Schema mismatches that cause null-return errors often share root causes with field selection errors — both stem from inconsistencies between declared types and actual resolver outputs. If you’re seeing multiple validation errors at once, check your type definitions first.

Analyzing Your Resolver Functions

Resolvers are where null values are born. The most robust resolvers follow a simple pattern: fetch data, validate it exists, return it or throw.

Problematic Resolver PatternImproved Resolver Pattern
return user.profile.namereturn user.profile?.name ?? 'Unknown'
return await api.getUser(id)const u = await api.getUser(id); if (!u) throw new GraphQLError('Not found'); return u;
return dbQuery.resultreturn dbQuery.result ?? throwError('User not found')

For non-nullable fields specifically: never rely on optional chaining alone. user.profile?.name returns undefined when profile is missing — which still triggers the error. You need either a real fallback value or an explicit error throw.

Error handling patterns within resolvers have evolved through community experience. Rather than allowing null values to propagate silently, professional developers implement explicit error boundaries that either provide fallback values or throw informative errors that help clients understand why data isn’t available.

Framework-Specific Solutions

The GraphQL spec defines what must happen when a non-nullable field returns null. Each framework decides how to give you tools to prevent or transform that situation. Here’s what works in each major ecosystem.

Apollo Server Solutions

Apollo Server provides multiple layers of error handling. The formatError hook intercepts errors globally before they reach the client — useful for sanitizing messages in production:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    if (formattedError.message.startsWith('Cannot return null')) {
      return {
        ...formattedError,
        message: 'Required data is temporarily unavailable',
        extensions: { code: 'DATA_UNAVAILABLE' }
      };
    }
    return formattedError;
  }
});
  • Use formatError to sanitize null-error messages in production — don’t expose schema internals
  • Set errorPolicy: 'all' in Apollo Client to receive partial data alongside errors
  • Use Apollo Server plugins to add null-error logging with full resolver context
  • Enable includeStacktraceInErrorResponses: false in production to hide internals
  • Apollo DevTools’ trace view shows resolver execution times — useful for spotting async failures

Client-side: setting errorPolicy: 'all' in Apollo Client lets your UI receive whatever data did resolve, alongside the error. Without this, Apollo Client defaults to returning null for the entire response when any error occurs — which is often worse than partial data.

Middleware Solutions for Absinthe (Elixir)

Absinthe’s middleware system lets you intercept and transform errors functionally, without touching individual resolvers. The Kronky library was built specifically for this:

defmodule MyApp.TranslateMessages do
  @behaviour Absinthe.Middleware

  def call(%{errors: errors} = resolution, _) do
    %{resolution | errors: Enum.map(errors, &translate_error/1)}
  end

  defp translate_error(%{message: "Cannot return null" <> _} = error) do
    %{error | message: "Required data is not available"}
  end
  defp translate_error(error), do: error
end
  1. Add Kronky to your mix.exs dependencies
  2. Create a TranslateMessages middleware module with the pattern above
  3. Register it in your Absinthe schema via middleware/2
  4. Add pattern-match clauses for specific null error messages you want to transform
  5. Test by deliberately returning nil from a non-nullable resolver field

Absinthe’s integration with Phoenix also means you can handle these errors consistently across GraphQL and REST endpoints using Phoenix’s error view infrastructure — useful when you’re building a hybrid API.

TypeORM and type-graphql Integration Issues

The TypeORM + type-graphql stack has specific pitfalls around nullability. The most common: TypeORM entity decorators and type-graphql @Field decorators can disagree about what’s nullable, and TypeORM’s lazy loading doesn’t play well with GraphQL’s execution model.

@Entity()
@ObjectType()
class User {
  @PrimaryGeneratedColumn()
  @Field(() => ID)
  id: number;

  @Column({ nullable: true })
  @Field({ nullable: true })      // ← must match Column config
  name?: string;

  @OneToMany(() => Post, post => post.author)
  @Field(() => [Post], { nullable: true })
  posts?: Post[];                 // ← don't rely on lazy loading here
}
  • Always match @Column({ nullable: true/false }) with @Field({ nullable: true/false })
  • Composite primary keys can cause partial entity loading — always verify all PK components are present
  • Replace TypeORM lazy relations with explicit DataLoader resolvers for GraphQL
  • Run synchronize: false in production and manage migrations explicitly to avoid schema drift
  • Use QueryBuilder.leftJoinAndSelect to eagerly load required relations

Solutions for Other GraphQL Implementations

  • graphql-java: Return DataFetcherResult.newResult().data(value).build() to combine data with errors; use DataFetchingEnvironment to add per-field error handling without throwing
  • GraphQL-Ruby: Use rescue_from in your schema class or return GraphQL::ExecutionError from resolvers instead of raising
  • GraphQL.NET: Implement IFieldMiddleware to intercept null returns before validation fires
  • Hasura: Use action handlers or event triggers with explicit null checks; set NOT NULL constraints in Postgres to enforce at the DB level
  • Express-GraphQL: Pass a customFormatErrorFn to the middleware to transform null errors
  • Relay: Use @catch directive (Relay 16+) to catch field errors at the component level without nulling the parent

Best Practices for Preventing This Error

The teams that rarely see this error in production share one habit: they treat nullability as a business contract decision, not a type-annotation habit. Before marking any field !, someone has to answer: “Can we guarantee this field will have a value for every record, past and future, including edge cases?”

  • Default to nullable; add ! only when the data guarantee is absolute
  • Wrap all external API calls in try/catch inside resolvers
  • Use middleware for cross-cutting null error logging — don’t repeat it per-resolver
  • Write unit tests that explicitly test resolver behavior when the data source returns null
  • Run integration tests against a database with realistic missing-data scenarios
  • Review nullability decisions during schema design reviews, not after production incidents

Schema Design Principles

The GraphQL community has largely converged on a “nullable by default” approach for application-level types. The reasoning: it’s always a non-breaking change to add ! later (clients that handle null will still work), but removing ! is technically breaking (clients may stop doing null checks they no longer need). Starting nullable and tightening over time is safer than the reverse.

  • Make fields nullable by default; non-nullable only when truly guaranteed
  • Apply ! to IDs, system timestamps, and database-enforced NOT NULL columns
  • Use union types (UserResult = User | NotFoundError) for explicit error modeling instead of nullable fields
  • Document every ! field with a comment explaining why the guarantee holds
  • Plan for schema evolution: will new data sources or migrations keep this guarantee?

Union types are worth adopting for any field where “not found” is a legitimate, expected outcome. Instead of making user: User nullable, you can declare user: UserResult where UserResult = User | UserNotFound. This makes the “no data” case explicit and typed, which is better for both the schema contract and the client developer experience.

Prevent null-return issues by designing explicit DTO layers that guarantee non-null fields at the boundary between your service and GraphQL resolvers. DTOs that enforce required fields at construction time surface the problem before it ever reaches GraphQL execution.

Implementing Robust Error Handling Patterns

A pre-deployment checklist for every non-nullable field:

  • DO: Test the resolver explicitly with a null/missing data source response
  • DO: Provide default values or throw GraphQLError with a useful extensions.code
  • DO: Use try/catch around every async operation that touches an external service
  • DON’T: Rely on optional chaining (?.) alone for non-nullable fields — it returns undefined
  • DON’T: Mark fields ! because they should always have a value — only if they do always have one
  • DON’T: Silently return null and let GraphQL surface the error — throw explicitly instead

Fallback strategies: for fields like display names or avatars, returning a computed default (“Anonymous”, a generic avatar URL) is often better than throwing an error. For fields like price or ID, throwing is correct — a product without a price is a data integrity problem that needs to surface. Match your strategy to the business meaning of the field.

When returning structured error responses for null violations, use ResponseEntity patterns in your Spring Boot layer to ensure consistent HTTP status codes and error payloads across your API surface.

Real-World Case Studies

Case Study: Fixing This Error in a Complex E-Commerce API

An e-commerce platform declared all inventory fields as non-nullable, based on the assumption that every product would always have stock data. During a major sales event, discontinued products started surfacing in search results — products that existed in the catalog but had been removed from the inventory system. Every query for these products failed with the null error, cascading to return null for entire product list pages.

  1. Read the error path: ["products", 0, "inventory", "quantity"] — pointed to Product.inventory resolver
  2. Ran the inventory query in isolation: confirmed it returned null rows for discontinued SKUs
  3. Changed inventory: Inventory! to inventory: Inventory in the schema
  4. Updated product card components to handle inventory: null as “Discontinued”
  5. Added a monitoring alert for null rates on previously-non-nullable fields
  6. Backfilled discontinued products with explicit inventory records marked status: DISCONTINUED

The long-term fix was step 6 — creating explicit inventory records for discontinued products rather than leaving gaps. This let them restore the non-nullable constraint, which is always preferable to permanently loosening it. The schema became more correct, not less strict.

“The Project.client is marked as non-null, meaning that this field will always have a value. That means the User.project has to be null.”
Advanced Web
Source link

Case Study: Resolving the Error in a Vulnerability Management System

A security platform required full audit trails for compliance: every vulnerability state transition had to have an author. The GraphQL schema declared author: User!. But automated security scans created state transitions without user accounts — no human initiated them.

Before (Problematic)After (Solution)
author: User! — no fallbackauthor: User! — resolver falls back to system user
return transition.authorreturn transition.author ?? systemUser
Automated transitions cause null errorAutomated transitions attributed to system@internal account

The key insight: making author nullable would satisfy GraphQL but violate compliance requirements (the audit trail needed every transition attributed to someone or something). Creating a dedicated system user account preserved both the non-nullable constraint and the compliance rule. The resolver was updated to detect automation-initiated transitions and assign them to the system account, keeping the schema strict while handling the edge case correctly.

Frequently Asked Questions

This error occurs when a GraphQL resolver returns null or undefined for a field declared non-nullable with ! in the schema. Common causes include database queries returning no rows, unhandled async errors in resolvers, missing foreign key relationships, or external API calls that fail. The GraphQL runtime enforces this constraint after all resolvers have run, before the response is sent to the client.

First, read the path array in the error response to identify the exact field. Then decide: if null is a legitimate state for that data, make the field nullable in the schema by removing !. If null represents a real data problem, fix the resolver to either return a valid default value or throw a GraphQLError explicitly. Never rely on optional chaining alone for non-nullable fields — it returns undefined, which also triggers the error.

A non-nullable field is declared with ! after the type — for example, name: String!. It means the field must always return a value; null is not permitted. The GraphQL server enforces this at runtime. Non-nullable fields are strongest for IDs and system-generated values where the data guarantee is absolute. For user-provided or relational data, nullable fields are usually safer.

This is GraphQL’s null propagation behavior, defined in the specification. When a non-nullable field returns null, GraphQL can’t leave the parent object in a partially-valid state — so it nulls the parent. If the parent is also non-nullable, the null propagates further up the tree, potentially reaching the root query. This behavior is intentional: it prevents clients from receiving silently broken partial data. To stop propagation, ensure an ancestor field in the chain is nullable.

Start with the path array in the error — it shows the exact field hierarchy where null occurred. Find the resolver for that specific type and field. Add a console.log immediately before the return statement to confirm what value is being returned. Then run the underlying database query or API call in isolation to verify whether null is coming from the data source. This narrows the fix to either the resolver logic or the data source.

Queries typically read existing data that satisfies non-nullable constraints, while mutations create or modify data and may return incomplete results if the operation partially fails. For example, a mutation that creates a record might return the new object before all required fields are populated, or it might fail partway through and return null. Check the mutation resolver’s return value carefully — it must return the full, valid object for all non-nullable fields, not just a status indicator.

Use ! (non-nullable) only when you can guarantee the field will have a value for every record, past and future — typically IDs, system-generated timestamps, and fields backed by database NOT NULL constraints. Default to nullable for user-provided data, optional profile fields, and relationship fields where the related entity might be deleted. It’s always safe to make a nullable field non-nullable later; the reverse is a breaking change for existing clients.