# When a write is refused

A write can be refused for two reasons, and a lifecycle transition for a
third, when its `when:` doesn't hold
([Transitions](the-operations-every-entity-gets.md#transitions), above):

- **The record isn't there.** That reports the entity's not-found
  failure, `TodoNotFound`. It includes a reference that names a missing
  record: a bad `project_id` is `ProjectNotFound`.
- **A database rule refuses it:** a unique rule, or a `restricted`
  reference blocking a delete.

Map each rule a write can break in its `conflict:` block, keyed by the
rule: `Project.name_per_owner` (a named unique rule), `User.email` (a
unique field) or `Todo.project_id` (a reference that blocks deleting a
project). The compiler knows which rules every write can break, so a
missing mapping is a compile error, and there is no catch-all. The
database's own error is kept as the failure's internal cause
([Failures](failures.md)).

Here creating a project can break the unique rule on its owner and name,
and closing one is blocked while todos still point at it. `owner.id` comes
from a loaded record, so the owner is surely there and the create can't
fail with `UserNotFound`:

```jadpo
action start_project(owner: User, name: Project.name) fails ProjectNameTaken -> Project {
    return attempt Project.create {
        set: {
            owner_id: owner.id  // a loaded record's id: the owner is there
            name: name
        }
        conflict: {
            Project.name_per_owner: ProjectNameTaken
        }
    }
}
```

```jadpo
action close_project(project: Project) fails ProjectHasTodos -> Unit {
    attempt Project.delete(project) {
        conflict: {
            Todo.project_id: ProjectHasTodos
        }
    }
}
```

What a write can break follows from what it writes:

- a create: every unique rule of its entity;
- an update: the unique rules over the fields it sets;
- a delete: each `restricted` reference to the record, or to a record its
  delete cascades to.

A reference's value surely names a record that is there when it is read
from a record loaded earlier in the action (`owner.id`, or a loaded
record's own reference to that entity), or when it is `none`. Any other
value may name a record that isn't there, so the write can fail with that
entity's not-found failure, which `attempt` passes on. A loaded record is
surely there because the transaction's snapshot holds it, and a
concurrent delete of it is a concurrent change
([All-or-nothing actions and concurrent changes](all-or-nothing-actions-and-concurrent-changes.md)).

The runtime maps the store's constraint name back to the rule, since
Jadpo names every constraint itself ([Data](data.md)).

## A failed action inside another

An action whose failure its caller handles with `match` runs inside a
savepoint, a point in the transaction it can be rolled back to. When it
fails, everything it wrote is undone, a refused write among them, and the
caller carries on in a transaction the failure didn't end. An action is
all-or-nothing whoever calls it.

An action that passes its failure on with `attempt` needs no savepoint:
the failure ends its caller too, up to a `match` that undoes both, or up
to the outermost action, which rolls back.
