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.
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:
InternalServerErrorsurfaces the original message and stack trace.AggregateGraphQLErrorincludes 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.