import { injectSymbolBasedInstanceOf } from "../internal/symbol-instanceof"; import type { Failure } from "./failure"; /** * A Nexus handler error. * * This error class represents an error that occurred during the handling of a * Nexus operation that should be reported to the caller as a handler error. * * Example: * * ```ts * import { HandlerError } from "nexus-rpc"; * * // Throw a bad request error * throw new HandlerError("BAD_REQUEST", "Invalid input provided"); * * // Throw a bad request error, with a cause * throw new HandlerError("BAD_REQUEST", "Invalid input provided", { cause }); * * // Throw a retryable internal error * throw new HandlerError("INTERNAL", "Database unavailable", { retryableOverride: true }); * * ``` * * @experimental */ export class HandlerError extends Error { /** * One of the predefined error types. * * If the constructor received an unknown string, this will be `"UNKNOWN"`. * Use {@link rawErrorType} to access the original string. * * @see {@link HandlerErrorType} */ public readonly type: HandlerErrorType; /** * The raw error type string. * * For known types, this equals {@link type}. For unknown types, this preserves * the original wire string while {@link type} is set to `"UNKNOWN"`. */ public readonly rawErrorType: string; /** * Whether this error should be considered retryable. * * By default, the retry behavior is determined from the error type. * For example, by default, `INTERNAL` is retryable, but `UNAVAILABLE` is non-retryable. * * If specified, `retryableOverride` overrides the default retry behavior determined based on * the error type. Use {@link retryable} to determine the effective retry behavior. * * @see {@link retryable}. */ public readonly retryableOverride?: boolean; /** * Set if this error was constructed from a {@link Failure} object. * * Preserves the original failure for round-tripping through the wire format. */ public readonly originalFailure?: Failure; /** * Constructs a new {@link HandlerError}. * * @param type - The type of the error. Must be a known {@link HandlerErrorType}. * @param message - The message of the error. * @param options - Extra options for the error, including the cause and retryable override. * * @experimental */ constructor(type: HandlerErrorType, message?: string | undefined, options?: HandlerErrorOptions) { const actualMessage = message || `Handler error: ${type}`; super(actualMessage, { cause: options?.cause }); if (!Object.hasOwn(HandlerErrorType, type)) { throw new TypeError(`Invalid HandlerErrorType: ${type}`); } this.type = type; this.rawErrorType = type === "UNKNOWN" ? (options?.rawErrorType ?? type) : type; this.retryableOverride = options?.retryableOverride; this.originalFailure = options?.originalFailure; if (options?.stackTrace !== undefined) { this.stack = options.stackTrace; } } /** * Whether this error is retryable. * * This differs from the {@link retryableOverride} property in that `retryable` takes into * account the default behavior resulting from the error type, if no override is provided. * * @see {@link retryableOverride}. */ public get retryable(): boolean { if (typeof this.retryableOverride === "boolean") return this.retryableOverride; switch (this.type) { case "BAD_REQUEST": case "UNAUTHENTICATED": case "UNAUTHORIZED": case "NOT_FOUND": case "NOT_IMPLEMENTED": case "CONFLICT": return false; case "UNAVAILABLE": case "UPSTREAM_TIMEOUT": case "RESOURCE_EXHAUSTED": case "INTERNAL": case "REQUEST_TIMEOUT": case "UNKNOWN": return true; default: { // Force a compile time error if missing a case const _noMissingCase: never = this.type; return true; } } } } injectSymbolBasedInstanceOf(HandlerError, "HandlerError"); /** * Options for constructing a {@link HandlerError}. * * @experimental * @inline */ export interface HandlerErrorOptions { /** * Underlying cause of the error. */ cause?: unknown; /** * Whether this error should be considered retryable. * * If not set, the retry behavior is determined from the error type. * For example, by default, `INTERNAL` is retryable, but `UNAVAILABLE` is non-retryable. */ retryableOverride?: boolean | undefined; /** * An optional stack trace string associated with this error. * * When provided, this overrides the native `stack` property on the error. * This is typically used for remote stack traces received over the wire, * which may originate from a different language runtime. */ stackTrace?: string; /** * An optional {@link Failure} object from which this error was constructed. * * Preserves the original failure for round-tripping through the wire format. */ originalFailure?: Failure; /** * The original error type string, preserving the raw wire value. * * For known types, this option is ignored. * When the error's type is set to `"UNKNOWN"`, this option is used to preserve the original wire string. */ rawErrorType?: string; } /** * An error type associated with a {@link HandlerError}, defined according to the Nexus specification. * * @experimental */ export type HandlerErrorType = (typeof HandlerErrorType)[keyof typeof HandlerErrorType]; export const HandlerErrorType = { /** * The error type is unknown. * * Subsequent requests by the client are permissible. */ UNKNOWN: "UNKNOWN", /** * The handler cannot or will not process the request due to an apparent client error. * * Clients should not retry this request unless advised otherwise. */ BAD_REQUEST: "BAD_REQUEST", /** * The client did not supply valid authentication credentials for this request. * * Clients should not retry this request unless advised otherwise. */ UNAUTHENTICATED: "UNAUTHENTICATED", /** * The caller does not have permission to execute the specified operation. * * Clients should not retry this request unless advised otherwise. */ UNAUTHORIZED: "UNAUTHORIZED", /** * The requested resource could not be found but may be available in the future. */ NOT_FOUND: "NOT_FOUND", /** * Returned by the server when it has given up handling a request. This may occur by enforcing * a client provided `Request-Timeout` or for any arbitrary reason such as enforcing some * configurable limit. * * Subsequent requests by the client are permissible. */ REQUEST_TIMEOUT: "REQUEST_TIMEOUT", /** * The request could not be made due to a conflict. This may happen when trying to create an * operation that has already been started. * * Clients should not retry this request unless advised otherwise. */ CONFLICT: "CONFLICT", /** * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system * is out of space. * * Subsequent requests by the client are permissible. */ RESOURCE_EXHAUSTED: "RESOURCE_EXHAUSTED", /** * An internal error occurred. * * Subsequent requests by the client are permissible. */ INTERNAL: "INTERNAL", /** * The server either does not recognize the request method, or it lacks the ability to fulfill the * request. Clients should not retry this request unless advised otherwise. */ NOT_IMPLEMENTED: "NOT_IMPLEMENTED", /** * The service is currently unavailable. * * Subsequent requests by the client are permissible. */ UNAVAILABLE: "UNAVAILABLE", /** * Used by gateways to report that a request to an upstream server has timed out. * * Subsequent requests by the client are permissible. */ UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT", } as const;