Skip to main content
Version: Next (2.x)

Error handling

@baeta/errors ships ready-made GraphQLError subclasses for the cases you hit on almost every API: unauthenticated, forbidden, bad input, internal failure. Each one tags extensions.code so clients can branch on the failure mode without parsing strings.

Available errors

UnauthenticatedError

Throw when authentication is required but missing. Sets extensions.code = "UNAUTHENTICATED" and HTTP status 401.

import { UnauthenticatedError } from "@baeta/errors";

const protectedDataQuery = Query.protectedData
.$use(async (next, { ctx }) => {
if (!ctx.user) {
throw new UnauthenticatedError();
// Default message: "Access denied! You need to be authenticated to perform this action!"
}
return next();
})
.resolve(({ ctx }) => {
return getProtectedData(ctx.user);
});

ForbiddenError

Throw when the request is authenticated but lacks permission. extensions.code = "FORBIDDEN".

import { ForbiddenError } from "@baeta/errors";

const adminDataQuery = Query.adminData
.$use(async (next, { ctx }) => {
if (!ctx.user.isAdmin) {
throw new ForbiddenError();
// Default message: "Access denied! You don't have permission to perform this action!"
}
return next();
})
.resolve(({ ctx }) => {
return getAdminData();
});

BadUserInput

Throw for invalid input like validation failures or malformed identifiers. extensions.code = "BAD_USER_INPUT".

import { BadUserInput } from "@baeta/errors";

const createUserMutation = Mutation.createUser.resolve(({ args }) => {
if (!isValidEmail(args.email)) {
throw new BadUserInput("Invalid email format");
}
// ...
});

InternalServerError

Wrap unexpected failures. In development the original message and stack trace surface; in production only the generic message reaches the client.

warning

Never run with development mode enabled in production. When dev mode is on, InternalServerError returns the original error's message and full stack trace in the GraphQL response, including DB driver errors, file paths, line numbers, and any sensitive data the underlying exception happened to carry. See Development mode for how the mode is determined.

import { InternalServerError } from "@baeta/errors";

const userQuery = Query.user.resolve(async ({ args }) => {
try {
return await getUserById(args.id);
} catch (error) {
throw new InternalServerError(error as Error, "Failed to fetch user");
}
});

AggregateGraphQLError

Reports several errors at once. Primarily used internally (e.g. when rule.or in @baeta/auth collapses multiple scope failures into a single response).

import { AggregateGraphQLError, BadUserInput } from "@baeta/errors";

const updateUserMutation = Mutation.updateUser.resolve(({ args }) => {
const errors: GraphQLError[] = [];

if (!isValidEmail(args.email)) {
errors.push(new BadUserInput("Invalid email format"));
}

if (!isValidName(args.name)) {
errors.push(new BadUserInput("Invalid name format"));
}

if (errors.length > 0) {
throw new AggregateGraphQLError(errors, "Validation failed");
}

// ...
});

Error Codes

Each error has a corresponding error code:

enum BaetaErrorCode {
Unauthenticated = "UNAUTHENTICATED",
Forbidden = "FORBIDDEN",
BadUserInput = "BAD_USER_INPUT",
AggregateError = "AGGREGATE_ERROR",
InternalServerError = "INTERNAL_SERVER_ERROR",
}

Development mode

Baeta checks BAETA_ENV, NODE_ENV, and ENVIRONMENT in that order of precedence to decide whether dev mode is on:

  • If any of the three is "production", dev mode is off (production wins, even if another var says "development").
  • Otherwise, if any of the three is "development", dev mode is on.
  • If none of them is set to either value, dev mode is off.

When dev mode is on, error responses include extra detail:

  • InternalServerError surfaces the original message and stack trace.
  • AggregateGraphQLError includes per-error stack traces.

For the other errors, your GraphQL server typically strips the trace before responding. In production these details are omitted.

Custom error options

All errors accept the standard GraphQLErrorOptions:

throw new UnauthenticatedError("Custom message", {
extensions: {
custom: "data",
},
});

HTTP status codes

UnauthenticatedError sets extensions.http.status = 401. The other errors leave the status to your GraphQL server's default.