Error Handling
Effect GraphQL provides structured error types that integrate seamlessly with GraphQL’s error system and Effect’s error channel.
Built-in Error Types
Section titled “Built-in Error Types”Effect GraphQL provides several error types out of the box:
import { GraphQLError, ValidationError, AuthorizationError, NotFoundError,} from "@effect-gql/core"GraphQLError
Section titled “GraphQLError”The base error type with support for extensions:
new GraphQLError({ message: "Something went wrong", extensions: { code: "INTERNAL_ERROR", timestamp: new Date().toISOString(), },})ValidationError
Section titled “ValidationError”For input validation failures:
new ValidationError({ message: "Invalid email format", field: "email", extensions: { code: "VALIDATION_ERROR", },})AuthorizationError
Section titled “AuthorizationError”For access control violations:
new AuthorizationError({ message: "You don't have permission to access this resource", extensions: { code: "FORBIDDEN", requiredRole: "ADMIN", },})NotFoundError
Section titled “NotFoundError”For missing resources:
new NotFoundError({ message: "User not found", resourceType: "User", resourceId: "123", extensions: { code: "NOT_FOUND", },})Using Errors in Resolvers
Section titled “Using Errors in Resolvers”Return errors using Effect.fail:
.query("user", { type: UserSchema, args: S.Struct({ id: S.String }), resolve: (args) => Effect.gen(function* () { const userService = yield* UserService const user = yield* userService.getById(args.id)
if (!user) { return yield* Effect.fail( new NotFoundError({ message: `User with id ${args.id} not found`, resourceType: "User", resourceId: args.id, }) ) }
return user }),})Error Transformation
Section titled “Error Transformation”Transform errors as they propagate using Effect combinators:
Catch Specific Errors
Section titled “Catch Specific Errors”resolve: (args) => Effect.gen(function* () { const db = yield* DatabaseService return yield* db.query(`SELECT * FROM users WHERE id = $1`, [args.id])}).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail(new GraphQLError({ message: "Database temporarily unavailable", extensions: { code: "SERVICE_UNAVAILABLE" }, })) ))Map All Errors
Section titled “Map All Errors”resolve: (args) => Effect.gen(function* () { // ... resolver logic}).pipe( Effect.mapError((error) => { if (error instanceof NotFoundError) { return error // Keep as-is } // Wrap unknown errors return new GraphQLError({ message: "An unexpected error occurred", extensions: { code: "INTERNAL_ERROR" }, }) }))Catch and Recover
Section titled “Catch and Recover”resolve: (args) => Effect.gen(function* () { const cache = yield* CacheService return yield* cache.get(args.key)}).pipe( Effect.catchAll(() => // Fallback to database if cache fails Effect.gen(function* () { const db = yield* DatabaseService return yield* db.get(args.key) }) ))Custom Error Types
Section titled “Custom Error Types”Create your own error types using Effect’s Data.TaggedError:
import { Data } from "effect"
class RateLimitError extends Data.TaggedError("RateLimitError")<{ readonly message: string readonly retryAfter: number}> {}
class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ readonly message: string readonly quota: number readonly used: number}> {}Use in resolvers:
resolve: (args) => Effect.gen(function* () { const rateLimiter = yield* RateLimiterService const allowed = yield* rateLimiter.check(args.userId)
if (!allowed.ok) { return yield* Effect.fail( new RateLimitError({ message: "Too many requests", retryAfter: allowed.retryAfter, }) ) }
// Continue with request...})Validation Errors
Section titled “Validation Errors”Arguments are validated automatically using Effect Schema. Validation errors are returned as GraphQL errors:
.mutation("createUser", { type: UserSchema, args: S.Struct({ email: S.String.pipe(S.pattern(/@/)), age: S.Int.pipe(S.positive()), }), resolve: (args) => { // This only runs if validation passes return Effect.succeed({ ... }) },})If validation fails, the client receives:
{ "errors": [ { "message": "Invalid argument: email must match pattern /@/", "extensions": { "code": "VALIDATION_ERROR" } } ]}Error Response Format
Section titled “Error Response Format”Errors are formatted according to the GraphQL spec:
{ "data": null, "errors": [ { "message": "User not found", "path": ["user"], "extensions": { "code": "NOT_FOUND", "resourceType": "User", "resourceId": "123" } } ]}Partial Errors
Section titled “Partial Errors”GraphQL supports partial responses where some fields succeed while others fail:
.query("dashboard", { type: DashboardSchema, resolve: () => Effect.gen(function* () { const userService = yield* UserService const statsService = yield* StatsService
// If stats fail, we still want user data const [user, stats] = yield* Effect.all([ userService.getCurrentUser(), statsService.getDashboardStats().pipe( Effect.catchAll(() => Effect.succeed(null)) ), ])
return { user, stats } }),})Global Error Handler
Section titled “Global Error Handler”When using the HTTP router, you can configure a global error handler for uncaught errors. This handles errors that occur outside of normal GraphQL execution (e.g., malformed requests, server errors).
Default Behavior
Section titled “Default Behavior”By default, uncaught errors return a 500 Internal Server Error:
{ "errors": [ { "message": "An error occurred processing your request" } ]}In non-production environments, the full error is logged for debugging.
Custom Error Handler
Section titled “Custom Error Handler”Provide a custom error handler to control the response:
import { makeGraphQLRouter, type ErrorHandler } from "@effect-gql/core/server"import { HttpServerResponse } from "@effect/platform"import { Cause, Effect } from "effect"
const customErrorHandler: ErrorHandler = (cause) => Effect.gen(function* () { // Log the error yield* Effect.logError("GraphQL request failed", cause)
// Check for specific error types const failure = Cause.failureOption(cause)
if (failure._tag === "Some" && failure.value instanceof RateLimitError) { return yield* HttpServerResponse.json( { errors: [{ message: "Rate limit exceeded", extensions: { code: "RATE_LIMITED" } }], }, { status: 429 } ) }
// Default error response return yield* HttpServerResponse.json( { errors: [{ message: "Internal server error" }], }, { status: 500 } ) }).pipe(Effect.orDie)
const router = makeGraphQLRouter(schema, serviceLayer, { errorHandler: customErrorHandler,})Error Handler Type
Section titled “Error Handler Type”The error handler receives an Effect Cause and must return an HttpServerResponse:
type ErrorHandler = ( cause: Cause.Cause<unknown>) => Effect.Effect<HttpServerResponse.HttpServerResponse, never, never>When the Error Handler is Called
Section titled “When the Error Handler is Called”The global error handler catches errors that escape normal GraphQL error handling:
- Malformed JSON in request body
- Missing required fields in request
- Errors during request parsing
- Uncaught exceptions in resolver execution
Using the Default Handler
Section titled “Using the Default Handler”You can import and customize the default handler:
import { defaultErrorHandler } from "@effect-gql/core/server"
// Use as-isconst router = makeGraphQLRouter(schema, serviceLayer, { errorHandler: defaultErrorHandler,})
// Or wrap itconst loggingErrorHandler: ErrorHandler = (cause) => Effect.gen(function* () { yield* Effect.logError("Unhandled error in GraphQL", cause) return yield* defaultErrorHandler(cause) })Best Practices
Section titled “Best Practices”1. Be Specific
Section titled “1. Be Specific”Use specific error types rather than generic ones:
// Goodnew NotFoundError({ message: "User not found", resourceType: "User" })
// Less helpfulnew GraphQLError({ message: "Not found" })2. Include Context
Section titled “2. Include Context”Add relevant information to error extensions:
new AuthorizationError({ message: "Access denied", extensions: { code: "FORBIDDEN", resource: "admin/users", requiredRole: "ADMIN", userRole: currentUser.role, },})3. Don’t Leak Internals
Section titled “3. Don’t Leak Internals”Transform internal errors before returning to clients:
Effect.catchAll((error) => { // Log the full error internally console.error("Internal error:", error)
// Return sanitized error to client return Effect.fail(new GraphQLError({ message: "An error occurred processing your request", extensions: { code: "INTERNAL_ERROR" }, }))})4. Use Error Boundaries
Section titled “4. Use Error Boundaries”Create error handling layers for different concerns:
const withAuthErrors = <A, E, R>(effect: Effect.Effect<A, E, R>) => effect.pipe( Effect.catchTag("AuthError", (e) => Effect.fail(new AuthorizationError({ message: e.message })) ) )
const withDatabaseErrors = <A, E, R>(effect: Effect.Effect<A, E, R>) => effect.pipe( Effect.catchTag("DatabaseError", (e) => Effect.fail(new GraphQLError({ message: "Database error", extensions: { code: "DATABASE_ERROR" }, })) ) )
// Use in resolverresolve: (args) => myLogic(args).pipe( withAuthErrors, withDatabaseErrors,)