---
url: https://docs.forestfuture.dev/d1-record/guide/callbacks.md
description: >-
  Registering callbacks, the order they run in, abort() to stop a save or
  destroy, and the callbacks not built yet.
---

# Callbacks

Callbacks run your code at points in a record’s lifecycle, such as before it’s validated or after it’s saved.

## Registering callbacks

Register callbacks in a Model’s `static {}` block. Each receives the record, and may be async:

```ts
import { abort, Model } from "@forestfuture/d1-record";

export class Order extends Model({
  id: { type: "integer", null: false },
  status: { type: "string", null: false, default: () => "pending" },
  email: { type: "string", null: false },
  totalCents: { type: "integer", null: false },
  suspended: "boolean",
  placedAt: "datetime",
}) {
  static {
    this.beforeValidation((order) => {
      order.email = order.email.trim().toLowerCase();
    });
    this.beforeCreate((order) => {
      order.placedAt = new Date();
    });
    this.beforeSave((order) => {
      if (order.suspended) throw abort("Orders can't change while suspended");
    });
    this.afterSave(async (order) => {
      if (order.hasSavedChangeTo("status", { to: "shipped" })) {
        await notifyShipped(order.email);
      }
    });
  }
}
```

Callbacks of one kind run in the order declared. A subclass runs its parent’s callbacks, then its own.

::: warning TIP
Once a Model has been used, registering another callback throws `InvalidModel`, so request code can’t add one that leaks into later requests.
:::

## Callback order

| Order          | Callback           | Runs                                                     | `abort()`                         |
| -------------- | ------------------ | -------------------------------------------------------- | --------------------------------- |
| **Saving**     |                    |                                                          |                                   |
| 1              | `beforeValidation` | Before the validators, on every save, and on `isValid()` | Stops the save                    |
| 2              | `afterValidation`  | After the validators                                     | Stops the save                    |
| 3              | `beforeSave`       | Before every write, new or persisted                     | Stops the save                    |
| 4              | `beforeCreate`     | Before inserting a new record                            | Stops the save                    |
| 4              | `beforeUpdate`     | Before updating a persisted record                       | Stops the save                    |
|                | *The write to D1*  | Skipped when a persisted record has no changes           |                                   |
| 5              | `afterCreate`      | After inserting a new record                             | Too late: `AbortAfterPersistence` |
| 5              | `afterUpdate`      | After updating a persisted record                        | Too late: `AbortAfterPersistence` |
| 6              | `afterSave`        | After every successful save                              | Too late: `AbortAfterPersistence` |
| **Destroying** |                    |                                                          |                                   |
| 1              | `beforeDestroy`    | Before the `dependent` strategies and the DELETE         | Stops the destroy                 |
|                | *The delete in D1* | With the dependents, in one batch                        |                                   |
| 2              | `afterDestroy`     | After the record is destroyed                            | Too late: `AbortAfterPersistence` |

A save runs either the create pair or the update pair, never both.

* **After-callbacks** run after every successful save, even one with no changes to write.
* **A before-callback that changes an attribute** makes the record dirty, and the change is written.
* **An after-callback that changes an attribute** leaves the record dirty. Nothing is written again.

::: warning Nothing is rolled back
D1 has committed a save’s writes before any after-callback runs. An error thrown by an after-callback reaches the caller, but the record stays saved.
:::

## Stopping a save

Throw `abort(reason)` from a before-callback to stop a save or destroy:

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

this.beforeSave((order) => {
  if (order.suspended) throw abort("Orders can't change while suspended");
});
```

* **`save()`** resolves to `false` and writes nothing. The record keeps its changes, and `record.abortReason` holds the reason.
* **`saveOrThrow()`** throws `RecordNotSaved` with the reason.
* **`destroy()`** resolves to `false`, and **`destroyOrThrow()`** throws `RecordNotDestroyed`.

Only `abort()` stops a save. Returning `false` does nothing, and any other error reaches the caller as it is.

::: warning TIP
In an after-callback, the write has already happened, so `abort()` throws `AbortAfterPersistence`, naming the callback.
:::

## Not built yet

These callbacks aren’t in d1-record yet. The code shows the shape they’re likely to take.

* **Around callbacks** ([#48](https://github.com/forestfuture/d1-record/issues/48)): `this.aroundSave(async (record, proceed) => { …; await proceed(); … })`, and `aroundCreate`, `aroundUpdate`, and `aroundDestroy`.
* **`afterInitialize` and `afterFind`** ([#49](https://github.com/forestfuture/d1-record/issues/49)): for every record built or loaded, and every record loaded.
* **`record.touch()` and `afterTouch`** ([#50](https://github.com/forestfuture/d1-record/issues/50)): sets `updatedAt` without a full save.
* **`on: "create"` and `on: "update"`** ([#51](https://github.com/forestfuture/d1-record/issues/51)): `this.validates("password", { presence: true, on: "create" })`. Until then, check `record.isNewRecord()` in an `if` option or a callback.

::: rails Compared with Rails
There’s no `after_commit` or `after_rollback`. In Rails, `after_save` runs before the save’s transaction commits. Here, D1 has already committed before any after-callback runs, so every after-callback is already “after commit”. In a [batch](./batches#batching-records), they run once the whole batch is written. When D1 refuses a write, nothing was written, and the save throws the [constraint error](./errors#constraint-errors).

Rails’ `if:` and `unless:` options for callbacks aren’t needed: write the condition inside the callback.
:::
