skip to content

What is Spring's ErrorType enum, how does it surface to clients, and why does an unhandled exception show as INTERNAL_ERROR?

level: seniorimportance: should knowfreq 34%

answer

  1. 5 values: BAD_REQUEST/UNAUTHORIZED/FORBIDDEN/NOT_FOUND/INTERNAL_ERROR
  2. implements graphql-java ErrorClassification
  3. surfaces under extensions.classification
  4. no HTTP status in GraphQL -> classification is the signal
  5. unhandled -> masked INTERNAL_ERROR + log with executionId

basics

~10 s

ErrorType is Spring's enum of error categories (BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR) implementing graphql-java's ErrorClassification. It appears to clients under extensions.classification. Unhandled exceptions default to INTERNAL_ERROR with a masked message for security.

solid answer

~40 s

org.springframework.graphql.execution.ErrorType is a Spring-provided enum implementing graphql-java's ErrorClassification interface, with five values: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR. You set it via GraphqlErrorBuilder.errorType(...); graphql-java serializes the classification into the error's extensions map under the "classification" key, giving clients a stable, machine-readable category (GraphQL has no HTTP status codes, so this is how you communicate error kind). When no resolver handles an exception, Spring's built-in fallback logs the real exception server-side with the executionId and returns a generic GraphQLError classified INTERNAL_ERROR whose message is masked (e.g. "INTERNAL_ERROR for <executionId>") to prevent leaking internal details. To expose a meaningful message and category you must explicitly map the exception via a resolver or @GraphQlExceptionHandler.

code

java · 20 lines
java
// Spring's built-in enum (values), used via GraphqlErrorBuilder:
// org.springframework.graphql.execution.ErrorType
//   BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR

@Component
class SecurityErrorResolver extends DataFetcherExceptionResolverAdapter {
    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        return switch (ex) {
            case AuthenticationException e -> build(env, ErrorType.UNAUTHORIZED, "Login required");
            case AccessDeniedException e   -> build(env, ErrorType.FORBIDDEN,   "Not allowed");
            default -> null; // -> falls back to masked INTERNAL_ERROR
        };
    }

    private GraphQLError build(DataFetchingEnvironment env, ErrorType type, String msg) {
        return GraphqlErrorBuilder.newError(env).errorType(type).message(msg).build();
        // client sees extensions.classification = UNAUTHORIZED / FORBIDDEN
    }
}

go deeper

for a junior

Know the five ErrorType values and that unhandled exceptions show as INTERNAL_ERROR.

for a middle

Explain classification appearing in extensions and why the default message is masked.

for a senior

Discuss ErrorType implementing ErrorClassification, custom classifications, and correlating INTERNAL_ERROR via executionId in logs.

for a principal

Treat the classification set as a versioned client contract and balance message exposure against information-leak risk across all mapping layers.

## Why classifications exist GraphQL responses are (almost) always HTTP **200** — there's no per-field HTTP status. So GraphQL needs an **in-payload** way to categorize errors. graphql-java models this with the **`ErrorClassification`** interface, exposed to clients under each error's **`extensions.classification`**. ## Spring's `ErrorType` enum `org.springframework.graphql.execution.ErrorType` is Spring's opinionated set of classifications, mirroring common HTTP semantics. It implements `ErrorClassification` and has **five** values: | ErrorType | Meaning (HTTP analogue) | |-----------|-------------------------| | `BAD_REQUEST` | client sent invalid input (400) | | `UNAUTHORIZED` | not authenticated (401) | | `FORBIDDEN` | authenticated but not allowed (403) | | `NOT_FOUND` | resource doesn't exist (404) | | `INTERNAL_ERROR` | server-side failure (500) | You pick one when building an error: ```java GraphqlErrorBuilder.newError(env) .errorType(ErrorType.BAD_REQUEST) .message("id must be positive") .build(); ``` graphql-java then places `"classification": "BAD_REQUEST"` in `extensions`. ## What the client sees ```json { "errors": [{ "message": "id must be positive", "path": ["book"], "extensions": { "classification": "BAD_REQUEST" } }] } ``` Clients branch on `extensions.classification` rather than string-matching messages. ## The default INTERNAL_ERROR and message masking If **no** resolver/handler in the chain resolves the exception, Spring's built-in `ExceptionResolversExceptionHandler` (wired into execution) does two things: 1. **Logs** the full exception server-side, tagged with the GraphQL **`executionId`** (so you can correlate). 2. Returns a **generic** `GraphQLError` classified **`INTERNAL_ERROR`** with a **masked** message like `"INTERNAL_ERROR for <executionId>"`. The masking is a **deliberate security default** — arbitrary exceptions might carry stack traces, SQL, file paths, or PII. Spring will not send those to clients. **You** decide, per exception type, what message and classification are safe to expose by mapping it explicitly. ## Custom classifications You aren't limited to the five built-ins. Because `ErrorClassification` is an interface, you can supply your own enum/implementation (e.g. `RATE_LIMITED`) via `.errorType(myClassification)`. Keep the set small and documented — it's a client contract. ## Gotchas - **INTERNAL_ERROR in dev is confusing**: seeing it usually means you forgot to map that exception, not that GraphQL is broken. Check the server log for the real cause under the executionId. - **classification lives in `extensions`, not top-level**: clients must read `error.extensions.classification`. - **HTTP status is separate**: over HTTP, Spring may still return 200 with errors; there are transport-level nuances (e.g. `WebGraphQlInterceptor` can set status), but classification is the primary error-kind signal. - **Don't leak by over-mapping**: if you blindly copy `ex.getMessage()` into the error for *all* exceptions, you reintroduce the leak the default protects against.

  • Where in the JSON response does a client read the error category?
    Under each error's extensions.classification, e.g. errors[0].extensions.classification == "NOT_FOUND". GraphQL has no per-error HTTP status, so classification is the machine-readable category.
  • Can you define a classification beyond the five built-in ErrorType values?
    Yes. ErrorType implements graphql-java's ErrorClassification interface, so you can pass a custom ErrorClassification (e.g. your own enum RATE_LIMITED) to GraphqlErrorBuilder.errorType(...). Keep the set small and documented as a client contract.

saying these in an interview costs you the question

  • Saying the classification appears at the top level of the error, not in extensions
  • Believing GraphQL returns a 500 HTTP status for a field error by default
  • Thinking INTERNAL_ERROR means the message was preserved — it's masked

context