---
url: https://docs.forestfuture.dev/d1-record/guide/persistence.md
description: >-
  save, create, update, and destroy, their OrThrow variants, what each returns
  or throws, lifecycle state, and reload.
---

# Persistence

Records are saved, updated, and destroyed with `save`, `create`, `update`, and `destroy`.

## Saving records

Save a record with `save`. It validates the record, runs its callbacks, and resolves to `true` or `false`:

```ts
import { User } from "./models";

const user = User.build({ email: "ada@example.com" });
if (await user.save()) {
  // saved: user.isPersisted() is true
} else {
  console.log(user.errors.fullMessages()); // ["Email has already been taken"]
}

const created = await User.create({ email: "grace@example.com" }); // saved, or not
await created.update({ isActive: false }); // true or false
```

A new record is inserted, and a persisted one updates only the attributes that changed. `save` resolves to `false` for one of two reasons:

* **Validation failed.** The reasons are in `record.errors`.
* **A callback aborted** with [`abort()`](./callbacks#stopping-a-save). The reason is in `record.abortReason`.

`create(attrs)` builds and saves a record, and resolves to it whether it saved or not. Check `isPersisted()` or `errors`. `update(attrs)` assigns, then saves.

## Throwing on failure

When a failure is exceptional, use the `OrThrow` version of each method:

```ts
import { RecordInvalid } from "@forestfuture/d1-record";
import { User } from "./models";

try {
  const grace = await User.createOrThrow({ email: "grace@example.com" });
  await grace.updateOrThrow({ isActive: true });
} catch (error) {
  if (error instanceof RecordInvalid) {
    return Response.json({ errors: error.record.errors.fullMessages() }, { status: 422 });
  }
  throw error; // anything else is a real failure
}
```

Instead of returning `false`, they throw:

* `RecordInvalid` when validation failed, with the record on `error.record`.
* `RecordNotSaved` when a callback aborted a save, with the abort’s reason as its message.
* `RecordNotDestroyed` from `destroyOrThrow()`, when a callback aborted the destroy or a `restrictWithError` association refused it.

::: rails Compared with Rails
The `OrThrow` methods are Rails’ `save!`, `create!`, `update!`, and `destroy!`.
:::

### What each method returns

| Method                                   | Resolves to                           | Throws                                                |
| ---------------------------------------- | ------------------------------------- | ----------------------------------------------------- |
| `save()` / `update(attrs)`               | `true` / `false`                      | constraint errors, `RecordDestroyed`                  |
| `saveOrThrow()` / `updateOrThrow(attrs)` | `true`                                | same as above, plus `RecordInvalid`, `RecordNotSaved` |
| `create(attrs)`                          | record, persisted or not              | constraint errors                                     |
| `createOrThrow(attrs)`                   | persisted record                      | same as above, plus `RecordInvalid`, `RecordNotSaved` |
| `destroy()`                              | destroyed record, or `false` on abort | constraint errors, `RecordDestroyed`                  |
| `destroyOrThrow()`                       | destroyed record                      | same as above, plus `RecordNotDestroyed`              |
| `find(id)`                               | record                                | `RecordNotFound`                                      |
| `findBy(conds)`                          | record or `null`                      | none                                                  |
| `findByOrThrow(conds)`                   | record                                | `RecordNotFound`                                      |

`false` only ever means validation failed or a callback aborted. Every method throws for anything else:

* **Constraint errors**, such as `RecordNotUnique`, since the database refused the write. See [Errors](./errors#constraint-errors).
* **An error thrown by a callback**, other than `abort()`, even from an after-callback once the record is saved.
* **D1’s own errors.**

## Destroying records

Destroy a record with `destroy`:

```ts
const destroyed = await created.destroy(); // the record, or false if a callback aborted
if (destroyed) {
  destroyed.isDestroyed(); // true: readable, but can't be changed or saved
}
```

It runs the destroy callbacks and the associations’ [`dependent` options](./associations#destroying-dependent-records), then deletes the row. A destroyed record’s attributes stay readable, but setting one, or calling `save`, `update`, `destroy`, or `reload`, throws `RecordDestroyed`.

::: warning TIP
If another request already deleted the row, `save` and `destroy` still succeed.
:::

## Checking a record’s state

| State                                            | `isNewRecord()` | `isPersisted()` | `isDestroyed()` |
| ------------------------------------------------ | --------------- | --------------- | --------------- |
| New (from `build()`, or after a failed `create`) | `true`          | `false`         | `false`         |
| Persisted (loaded, or after a successful save)   | `false`         | `true`          | `false`         |
| Destroyed (after a successful `destroy()`)       | `false`         | `false`         | `true`          |

## Reloading records

Load a record’s row again with `reload`, discarding unsaved changes and loaded associations:

```ts
import { User } from "./models";

const fresh = await User.find(user.id);
fresh.email = "changed@example.com";
await fresh.reload(); // back to what D1 holds
```

It throws `RecordNotFound` if the row is gone, or the record was never saved.

## Writing many rows

To insert, update, or delete many rows at once, without validations and callbacks, use `insertAll`, `upsertAll`, `updateAll`, and `deleteAll`. See [Bulk writes and batches](./batches).
