Frontend client

Client errors

The generated client's error classes, how to branch on them with instanceof, what cause and issues carry, AvTransportError versus AvError, and AvContractMismatchError.

Read as Markdown

A client call resolves to the data, or throws. There is no { data, code, cause } envelope to inspect: the generated client unwraps successes and turns every failure into a typed error. Branch on the class (or on code), never on the message text.

Code
import avClient, { AvNotFoundError } from "./api/AvClient";

try {
  await avClient.User.find.unique({ where: { id: 9999 } });
} catch (e) {
  if (e instanceof AvNotFoundError) {
    console.log(e.code);           // "A2003"
    console.log(e.cause.message);  // "No record matched the unique selector."
  } else {
    throw e;
  }
}

The classes

All are exported from AvClient.ts. Every class except AvTransportError extends AvError and carries code and cause.

ClassCodesThrown when
AvValidationErrorA2004, A2007, A2009The request breaks a contract rule: unknown field, wrong type, field not selectable, bad $ref, limit exceeded, or a field validator, which the client checks before it sends. cause.issues lists the problems.
AvNotFoundErrorA2003A unique operation matched nothing, or a $connect names a record that does not exist.
AvConflictErrorA2008, A2013, A2014A uniqueness or reference constraint (A2008), a concurrent modification (A2013) or a stale selection (A2014). The last two may succeed if retried.
AvAuthErrorA4000, A4001, A4002A pipeline guard or the configuration's policy refused: unauthenticated, forbidden, policy denied.
AvContractMismatchErrorA2005The generated client is stale. Regenerate.
AvProtocolErrorA2000, A2001, A2002, A2006, A2010, A2011, A2012The request could not be understood or routed. Rare with a generated client.
AvInternalErrorA3000 to A3004An unexpected server or adapter failure.
AvTransportErrornoneNo framework answer arrived.

The full code table, with HTTP statuses, is in HTTP protocol.

Refused before anything is sent

The client checks a few things locally and throws before any request is made: validators listed in the contract (V1004 and the other validator codes), and an undefined in a filter, a unique selector or a cursor (A2004 / V1001 at that path, Filtering). These are AvValidationErrors with the same code and path the server would answer.

A TypeError before anything is sent

One refusal is not an AvError at all. The client reads the arguments you pass it through a guard, and a value that cannot be read (a revoked Proxy, a getter that throws) is refused with a TypeError that names where, before a request is made:

Code
TypeError: An operation's arguments must be readable plain data, and the value at /where/id could not be read.

A tx builder never throws for this either: avClient.transaction([...]) rejects with that TypeError. Server-side calls answer the same mistake as A2004 / V1001 instead (Server-side usage).

cause and issues

AvError.cause has this shape:

Code
interface AvCause {
  readonly message: string;
  readonly issues?: readonly AvValidationIssue[];  // validation failures
  readonly operation?: number;                   // transaction step that failed
}
interface AvValidationIssue {
  readonly path?: readonly (string | number)[];
  readonly code: AvValidationCode;                 // "V1001", ...
  readonly message: string;
  readonly check?: string;                         // a failed field validator: zod's check name
  readonly params?: Record<string, unknown>;       // and its parameters
}

A validation failure, as thrown for where: { email: { contains: 5 } }:

Code
// AvValidationError, e.code === "A2004"
// e.cause === {
//   message: "Operation arguments failed framework validation.",
//   issues: [{
//     path: ["arguments", "where", "email", "contains"],
//     code: "V1001",
//     message: "Value must be a string."
//   }]
// }

A failed field validator also says which rule broke, so a form can show its own text:

Code
// thrown before any request, by the client's own check
// e.cause.issues[0] === {
//   code: "V1004", path: ["arguments", "data", "name"],
//   message: "Value must be at least 3 characters long.",
//   check: "min_length", params: { minimum: 3 }
// }

The server would answer the same issue, so the client and server checks read alike. When the issue comes from your own pipeline code, message is the text you wrote, if it was non-blank and at most 1,000 characters; otherwise it is the standard text for the code. Use path to attach the message to a form field:

Code
import { AvValidationError } from "./api/AvClient";

function fieldErrors(e: unknown): Record<string, string> {
  if (!(e instanceof AvValidationError)) return {};
  const out: Record<string, string> = {};
  for (const issue of e.cause.issues ?? []) {
    // paths look like ["arguments", "data", "email"]
    const field = issue.path?.[issue.path.length - 1];
    if (typeof field === "string") out[field] = issue.message;
  }
  return out;
}

A transaction error carries cause.operation, the zero-based index of the failing step (Transactions).

Handling the common cases

Code
import avClient, {
  AvAuthError, AvConflictError, AvContractMismatchError, AvNotFoundError,
  AvTransportError, AvValidationError,
} from "./api/AvClient";

try {
  await signedInClient.User.create.one({ data: { email: "ada@example.com" } });
} catch (e) {
  if (e instanceof AvConflictError) {
    // A2008: "The operation conflicts with the current state of the resource." (duplicate email)
  } else if (e instanceof AvAuthError) {
    // A4000: "Sign in first."  -> send the user to the login screen
  } else if (e instanceof AvValidationError) {
    // show e.cause.issues
  } else if (e instanceof AvContractMismatchError) {
    // the app is older than the API: ask for a reload / redeploy
  } else if (e instanceof AvTransportError) {
    // offline, DNS, CORS, or a non-Aventara answer
  } else {
    throw e;
  }
}

AvAuthError comes from your own guards (Configuration); the message is whatever your guard threw. The same holds for the message of each issue a pipeline throws (AvError("A2004", { message, issues })), when its path names a member of the request's arguments. Without a guard nothing throws it.

AvTransportError versus AvError

AvError familyAvTransportError
MeaningThe framework answered, and said noNo framework answer arrived
Hascode, causestatus (HTTP status or null)
RemedyBy codeCheck the network, URL and server

AvTransportError covers:

  • A failed request (server down, refused connection, CORS failure in a browser): status is null, message The request for "User" find.many failed before any response arrived.
  • A response that is not an Aventara envelope, such as a proxy's or a host's own 404 for a path it does not mount: status is the HTTP status, message The response to "User" find.many (HTTP 404) is not an Aventara response envelope.
  • A value in a response that cannot be read.

An AvTransportError names no remedy: nothing in it says the client is stale or the server is wrong.

An aborted call is neither: it rejects with exactly what fetch rejected with, an AbortError DOMException (Client options).

AvContractMismatchError

A generated client is bound to the contract hash and protocol version it was generated from, and sends both on every request. When the server's contract differs, it refuses with A2005:

Code
AvContractMismatchError: Generated client contract does not match the server. Regenerate the client.

This also happens when the client calls an operation the server no longer advertises, because the server checks the client's identity before it routes. The remedy is always the same: regenerate against the deployment the client calls (Keeping in sync).

No automatic retries

The client never retries. AvConflictError with A2013 or A2014 is the one case where retrying the same call can succeed; do it yourself, with a limit.

See also