---
url: https://docs.forestfuture.dev/d1-record/guide/errors.md
description: >-
  Every error d1-record throws: lifecycle, usage, and constraint errors, and
  when each is raised.
---

# 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

| Error                   | When                                                                     |
| ----------------------- | ------------------------------------------------------------------------ |
| `RecordNotFound`        | `find`, `findByOrThrow`, or `reload` found nothing                       |
| `RecordInvalid`         | an `OrThrow` save found the record invalid; the record is on `.record`   |
| `RecordNotSaved`        | a save was aborted by a callback, or needs an owner that isn’t saved     |
| `RecordNotDestroyed`    | a destroy was aborted, or restricted by `restrictWithError`              |
| `RecordDestroyed`       | writing to a record that’s already destroyed                             |
| `CallbackAbort`         | what `abort()` makes; caught by the lifecycle                            |
| `AbortAfterPersistence` | `abort()` in an after-callback, too late to stop the write (`.callback`) |

## Usage errors

A usage error means the code needs to change:

| Error                  | When                                                                            |
| ---------------------- | ------------------------------------------------------------------------------- |
| `InvalidModel`         | a Model’s declaration is wrong, detected the first time it’s used               |
| `UnknownAttribute`     | an attribute that isn’t declared, in `build`, `where`, `update`, …              |
| `MissingAttribute`     | reading an attribute that `select` didn’t load                                  |
| `UnknownAssociation`   | an association that isn’t declared                                              |
| `AssociationNotLoaded` | `loaded()` on an association that wasn’t loaded                                 |
| `IncompatibleRelation` | two Relations or writes can’t be used together (below)                          |
| `IrreversibleOrder`    | `last()` on a Relation ordered by an `sql` fragment                             |
| `DeleteRestriction`    | destroying an owner whose `restrictWithException` association still has records |
| `NoDatabase`           | using a record made with `new` rather than `build`                              |
| `NoSession`            | `db.bookmark()` on a Database opened without a session                          |

::: warning 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`:

| Error               | Constraint              |
| ------------------- | ----------------------- |
| `RecordNotUnique`   | `UNIQUE`, `PRIMARY KEY` |
| `InvalidForeignKey` | `FOREIGN KEY`           |
| `NotNullViolation`  | `NOT NULL`              |
| `CheckViolation`    | `CHECK`                 |

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.
