Skip to content

Errors ​

Every error the library throws extends EgonError. Each has a stable code for its class, a stable id for its message, a short message that names what failed and links to the entry of its id, and, where a driver error was involved, that error as cause.

ts
try {
  await repo.insertOrThrow(cs);
} catch (error) {
  if (error instanceof InvalidChangesetError) return error.changeset.errorMessages();
  if (error instanceof ConstraintError) return { [error.column ?? "$base"]: [`violates ${error.type}`] };
  if (error instanceof DatabaseError && error.retryable) return "busy: try again";
  throw error;
}

isEgonError(value, code?) checks by code. It works across two copies of the package, where instanceof would not:

ts
// Works when two copies of the package are loaded, where instanceof would not.
if (isEgonError(error, "noResults")) return 404;
if (isEgonError(error) && error.code === "timeout") return 503;

Identifiers and messages ​

Each distinct message has an identifier, error.id, such as "query.aliasTaken": the namespace of its class and a name, or the class's code alone for a class with one message ("noResults"). It never changes between releases, and its type, ErrorId, is the union of them all, so a switch on it is checked:

ts
if (isEgonError(error) && error.id === "query.aliasTaken") return "pick another alias";

A message reads <id>: <short sentence> See <link>:

text
query.aliasTaken: join() was given the alias "u", which this query already uses. See https://egon-orm.github.io/egon-orm/reference/errors/query#query-aliastaken

The short sentence says what failed, with the names it concerns (the clause, the schema, the field, the SQL state). What to do about it is on the identifier's entry, one page per namespace:

PageThrown as
GeneralNoResultsError, MultipleResultsError, InvalidChangesetError, StaleEntryError, NotLoadedError
QueryQueryError
CastCastError
Schema definitionSchemaDefinitionError
ConstraintConstraintError
DatabaseDatabaseError
RollbackRollbackError
MigrationMigrationError

Short messages keep the bundle small: when every message carried its advice, error text was about 15% of the library's minified size, and an application that runs on the edge ships its bundle with every deployment.

Full messages, in development ​

The advice ships separately, in egon-orm/messages. There are two ways to use it, and both are explicit.

For every error: useFullErrorMessages(messages) makes every error constructed after the call carry the full text: the short sentence, what to do about it, and the link.

ts
// In the first module the application imports, before any schema is defined:
if (development) useFullErrorMessages(messages);

It works on every runtime (it reads nothing from process), on Cloudflare Workers and Vercel Edge too. What brings the catalog into a bundle is importing egon-orm/messages (for messages or for explain); an application that never imports it ships no advice.

Call it before your schemas are defined

An error made before the call keeps its short message. A schema whose definition is wrong throws while its module is imported, which can happen before the code that calls useFullErrorMessages runs: call it in the first module your application imports, or use explain(error).

For one error: explain(error) returns the full text of one error, whenever it was made, without changing any setting. It is the form for a log line or an error page:

ts
try {
  await repo.insertOrThrow(cs);
} catch (error) {
  console.error(explain(error)); // the full text of this one error, whenever it was made
  throw error;
}

explain takes any value: an Egon error gives its full text (or its message as it is, when the catalog has no entry for its identifier, or it was made by another copy of the package); any other Error gives name: message; anything else String(value).

Both take a catalog of your own instead, with the same shape: for an identifier, the advice that follows the short sentence. in an entry is replaced by a parameter the error carries (a name such as the field or the column; the generated pages list each entry with its placeholders). An identifier your catalog leaves out keeps its short message:

ts
useFullErrorMessages({
  noResults: "Es wurde keine Zeile gefunden.",
  "query.aliasTaken": "Jede Bindung braucht einen eigenen Alias.",
});

Sending an error to a client ​

JSON.stringify(error) gives { name, code, id } and nothing else: the message, the cause and any SQL stay on the server. It is safe to send, and stable to switch on:

ts
// { "name": "NoResultsError", "code": "noResults", "id": "noResults" }: no message, cause or SQL.
if (isEgonError(error)) return new Response(JSON.stringify(error), { status: 400 });

Classes ​

ErrorcodeThrown whenExtra fieldsHow to fix
InvalidChangesetError"invalidChangeset"An OrThrow write or applyActionOrThrow is given an invalid changeset, or a declared constraint is violated.changeset, actionShow error.changeset.errorMessages(), or use the method without OrThrow and check the result.
ConstraintError"constraint"The database rejects a write and the changeset did not declare that constraint; also every violation in a bulk write.type, constraint, table, column, detail, sqlDeclare it on the changeset (the full message says how). See Constraints.
StaleEntryError"staleEntry"An update or delete matched no row: the row was deleted, or an optimistic lock failed.action, schemaReload and retry, or pass staleError / allowStale. See Update.
NoResultsError"noResults"oneOrThrow, firstOrThrow, getOrThrow, getByOrThrow or reloadOrThrow found nothing.query (rendered, without values)Use the method without OrThrow where a missing row is expected.
MultipleResultsError"multipleResults"one, getBy or their twins matched more than one row.count, queryMake the query unique, or use first for "any one".
QueryError"query"A query, changeset, batch or transaction is built or used wrongly, or needs a feature the database lacks.The entry of error.id says what to do. See Dialect support for features.
CastError"cast"cast is given params that are not an object, or a stored value does not fit its field's type.Pass a plain object; check the column holds what the field declares.
NotLoadedError"notLoaded"castAssoc or putAssoc is used on an association of a loaded row that was not preloaded as whole rows.schema, associationPreload the association before building the changeset.
SchemaDefinitionError"schemaDefinition"A schema, type or association is defined incorrectly, or a schema's constructor is called.The message names the schema and the field; the entry of error.id says what to do.
RollbackError"rollback"transactionOrThrow ended in a rollback: tx.rollback(reason), a batch step that failed (reason is { step, error, completed }), or a statement that failed and was not handled.reasonUse transaction to get the rollback as a result.
DatabaseErrorsee belowThe connection or the database failed.retryable, sqlDepends on the code.
MigrationError"migration"A migration has no up or no down, uses a feature the database lacks, failed, is missing its file, or the migration lock could not be taken. Imported from egon-orm/migration.version, migration, sqlThe message says which migration and why.

Every class also has id and toJSON().

DatabaseError codes ​

CodeMeaningretryable
"database"A driver error with no more specific code.no
"connection"Could not connect, the connection was lost, or the repo was closed.no
"serializationFailure"The transaction conflicted with another.yes
"deadlock"The database broke a deadlock by cancelling this transaction.yes
"aborted"The query was cancelled by the caller's AbortSignal.no
"timeout"The statement, or the whole call, ran longer than its timeoutMs.no

retries on a transaction runs it again for the retryable codes.

What is not thrown ​

Invalid input and declared constraint violations are returned, not thrown, by the methods without OrThrow:

ts
const result = await repo.insert(cs);
if (!result.success) result.error.errorMessages(); // { email: ["has already been taken"] }

See Writing. Their messages (can't be blank, has already been taken) are data for your users, and are not affected by any of the above.

What errors contain ​

  • The SQL that failed (sql), where there was one.
  • The driver's error as cause.
  • Never the parameter values, unless the repo was created with log: { params: true }.
  • Never the value of a redacted field. A database message that may quote a value is left out of the message and kept on cause.

See also ​