Skip to content

Errors ​

Every error d1-record throws extends D1RecordError, so one check separates them from anything else:

ts
import { D1RecordError } from "@forestfuture/d1-record";

if (error instanceof D1RecordError) {
  // one of d1-record's own
}

Lifecycle errors ​

ErrorWhen
RecordNotFoundfind, findByOrThrow, or reload found nothing
RecordInvalidan OrThrow save found the record invalid; the record is on .record
RecordNotSaveda save was aborted by a callback, or needs an owner that isn’t saved
RecordNotDestroyeda destroy was aborted, or restricted by restrictWithError
RecordDestroyedwriting to a record that’s already destroyed
CallbackAbortwhat abort() makes; caught by the lifecycle
AbortAfterPersistenceabort() in an after-callback, too late to stop the write (.callback)

Usage errors ​

A usage error means the code needs to change:

ErrorWhen
InvalidModela Model’s declaration is wrong, detected the first time it’s used
UnknownAttributean attribute that isn’t declared, in build, where, update, …
MissingAttributereading an attribute that select didn’t load
UnknownAssociationan association that isn’t declared
AssociationNotLoadedloaded() on an association that wasn’t loaded
IncompatibleRelationtwo Relations or writes can’t be used together (below)
IrreversibleOrderlast() on a Relation ordered by an sql fragment
DeleteRestrictiondestroying an owner whose restrictWithException association still has records
NoDatabaseusing a record made with new rather than build
NoSessiondb.bookmark() on a Database opened without a session

TIP

A name from a request, such as ?include= or a JSON key, only ever matches the Model’s own attributes and associations; constructor and __proto__ are unknown like any other. Return a 400 for these errors.

When Relations are incompatible ​

IncompatibleRelation is thrown for combinations that can’t make a correct query or write:

  • or(), merge(), or a scope with a Relation of another Model, or one that resolves to another database when it runs. or() also needs both sides to have the same select, order, limit, offset, distinct, and includes, since only their conditions can be combined.
  • batch([...]), db.batch([...]), save(), or destroy() with statements or records from two databases, such as a belongsTo target built on another connect(). A write runs on one database. The message names where each database came from.
  • includes with select(...).distinct(). Preloading needs the keys, and adding them would undo the distinct.
  • Preloading an association scope that uses limit, offset, or distinct. One preload query serves every owner, so the limit would apply to all of their records together.
  • updateAll or deleteAll after distinct(): they reject with it, and toUpdateAll/toDeleteAll throw it.

Constraint errors ​

When D1 refuses a write because of a constraint, it throws a subclass of ConstraintViolation:

ErrorConstraint
RecordNotUniqueUNIQUE, PRIMARY KEY
InvalidForeignKeyFOREIGN KEY
NotNullViolationNOT NULL
CheckViolationCHECK

Each has D1’s original error as cause, the table, and, where D1’s message names them, the columns and attributes:

ts
import { RecordNotUnique } from "@forestfuture/d1-record";

try {
  await user.save();
} catch (error) {
  if (error instanceof RecordNotUnique && error.attributes.includes("email")) {
    user.errors.add("email", "has already been taken", { type: "taken" });
  } else {
    throw error;
  }
}

Every method throws them, save() as well as saveOrThrow(). The record keeps its changes, so you can fix them and save again.