# Errors in practice

How to catch ARX's errors, what each kind means for your application, when to retry, and what a first connection's
errors say.

## Catch on the base class, by code

Every error an ARX call throws is an `ArxException` with a stable `Code`: the same number and name in every provider.
(A misuse found while the schema catalog is built at startup, such as a table declared twice, is an `ArgumentException`,
as .NET builders report one.) Catch the base class and filter by code:

```csharp
try
{
    await service.UpdateAsync(change);
}
catch (ArxException e) when (e.Code == ArxErrorCode.RecordNotFound)
{
    // the record isn't there
}
```

Some codes come as a subclass (`ArxValidationException` when ARX refused the call before the database,
`ArxDatabaseException` when the database refused it, `ArxNotSupportedException` for ARX8001), but not every one: a
record not found (ARX2002) can come as the base class. A `catch` of a subclass can miss an error, so filter on the code.

The thousands digit is the `Category`: 1 validation, 2 not found, 3 constraint, 4 data, 5 schema, 6 permission,
7 transient, 8 not supported, 9 other. The message starts with the code (`ARX3001 UniqueViolation: …`), names the table
and column where it can, and is written not to repeat the values you sent, so it is safe to log. The database's own error stays on the exception: its
native code (`SqlState` or `NativeCode`) and the driver's exception as `InnerException`.

That holds for `Message`, not for `InnerException`: the driver's own message can quote values. MySQL's and MongoDB's do
for a duplicate key (`Duplicate entry 'A-100'`, `dup key: { account_number: "A-100" }`); PostgreSQL's hides them by
default. So log `exception.Message` and `exception.Code`, not `exception.ToString()`, when values may be personal data.

The Error codes page of each provider lists every code with the database errors it comes from.

## Which table and column

`EntityName` and `AttributeName` are filled when ARX knows them. That covers everything ARX checks itself: names, value
types against a catalog that declares the columns, `GetAttributeValue<T>`, and an undeclared table. It also covers a
column the database says is missing (ARX5002); when only a link names it, the error names the link's table and alias.
An error the database raises names what the database says:

- A unique violation (ARX3001) carries `ConstraintName` (`customer_email_key`), not the column. To show "email already
  in use" next to a form field, map your constraint names to fields.
- A value too long (ARX4002), or of the wrong type (ARX4004) for a column the catalog doesn't type: PostgreSQL doesn't
  say which column, so `AttributeName` is empty. With the schema preloaded with `DataTypes`, or the columns declared,
  ARX checks a value's type before the database and names the column. A value's length is still the database's check.

## A Multiple call that fails

`CreateMultipleAsync`, `UpdateMultipleAsync` and `DeleteMultipleAsync` are all or nothing. A database error doesn't say
which entity caused it, because the database checks the statement as a whole. When you need to know, for an import
report say, send the entities one call each in a batch scope with savepoints: the failed call's error has its
`BatchNumber`, and the others still commit.

```csharp
await service.RunBatchAsync(async scope =>
{
    foreach (var entity in entities)
    {
        try
        {
            await scope.CreateAsync(entity);
        }
        catch (ArxException error) when (error.Category is ArxErrorCategory.Constraint or ArxErrorCategory.Data)
        {
            report.Add((error.BatchNumber, error.Code));   // the entity at that position wasn't written
        }
    }
}, new ArxBatchOptions { Savepoints = true });
```

Savepoints are on PostgreSQL, SQL Server and MySQL. MongoDB and DynamoDB refuse them (ARX8001): send one call per
entity outside a scope there.

## When the record isn't there

| Call | Result |
|---|---|
| `RetrieveAsync` | `null` |
| `RetrieveMultipleAsync` | an empty collection |
| `UpdateAsync` | ARX2002 RecordNotFound; nothing is created |
| `UpdateMultipleAsync` | ARX2002 if any entity matches nothing; nothing changes |
| `DeleteAsync(table, id)` | `false` |
| `DeleteAsync(entity)` | `0`; with `RequireSingleRow = true`, ARX2002 |
| `DeleteMultipleAsync` | how many existed and went |
| An update or delete whose entity carries an old version (a table with `.Version(…)`) | ARX2004 RecordChanged: the record is there, at another version; nothing is written |

A delete that finds nothing isn't an error: deleting something already gone (a retry, two users) is the same end
state. When your application needs the record to have been there, use `RequireSingleRow`. An update that finds
nothing is: it would otherwise lose the change without a word.

## When someone else changed the record

Declare a version column on a table (`.Version("account", "row_version")`, an integer column) and ARX keeps it: a create
writes 1, every update adds one. Read the record, change it, and update it with the version it was read with; if another
writer got there first, the update is ARX2004 and writes nothing:

```csharp
try
{
    account["name"] = "Northwind Traders Ltd";   // account was read with its row_version
    await service.UpdateAsync(account);
}
catch (ArxException error) when (error.Code == ArxErrorCode.RecordChanged)
{
    // Read it again, apply the change to what is there now (or ask the user), and update again.
}
```

An update without the version isn't checked (the last writer wins) but still moves the version.
`.Version("account", "row_version", required: true)` refuses such an update (ARX1001). Every provider from
0.1.0-preview.4; DynamoDB only in earlier versions.

## In a web API

A starting point for turning codes into HTTP statuses. Map by code, not by category: the NotFound category (2xxx) also
holds ARX2003 and ARX2004, which are conflicts, not missing records.

| Codes | Meaning for the caller | Status |
|---|---|---|
| ARX2002 RecordNotFound | not there | 404 |
| ARX2003 MultipleRecordsMatched | the key you matched by isn't unique | 409 |
| ARX2004 RecordChanged | changed since it was read: read again | 409 (or 412 with `If-Match`) |
| ARX3001 UniqueViolation, ARX3002 ForeignKeyViolation | conflicts with other data | 409 |
| ARX3003, ARX3004, 4xxx | a value the table refuses | 400 or 422 |
| ARX1005 InvalidLookup, ARX1007 UnsupportedValueType, ARX1011 SelectionSetValueNotAllowed, ARX1013 InvalidDateTimeValue | a value refused before the database: an option the set doesn't allow, an empty date, a reference to a record that can't exist | 400 or 422 when the value came from the request |
| ARX1001 InvalidArgument | a paging value below 1, or another argument out of range | 400 when it came from the request (page size, page number) |
| ARX1017 SuspectedInjection | a name carrying SQL or query syntax: log it | 400 |
| other 1xxx, 5xxx, 6xxx, 8001, 9001 | a fault in the application or its setup: log it | 500 |
| 7xxx (`IsTransient` is true) | try again later | 503 |

The other 1xxx codes (an undeclared table, an invalid name your code built, a connection string missing) are the
application's own mistakes rather than the caller's, so treat them as 500 unless the name came from the request. The
same goes for the value codes above when your own code built the value: then it is a bug, not bad input.

## Retrying

Retry only when `IsTransient` is true: a connection that failed, a deadlock or serialization failure, throttling, a
timeout. Retry the whole unit of work (the transaction or batch scope, from its start), with a backoff.

ARX doesn't retry your calls or your transactions. Two things below it do: the drivers (the AWS SDK retries throttled
DynamoDB requests, and the MongoDB driver retries a write once after a network error), and, on MongoDB only, the
transaction ARX opens itself for a call that writes several documents outside your transaction: ARX runs that
transaction once more when MongoDB labels the failure `TransientTransactionError`, and commits it once more on
`UnknownTransactionCommitResult`, as MongoDB recommends (the whole transaction, so nothing is written twice). A
transaction of yours (`BeginTransactionAsync`) is never retried. So an ARX7xxx error has usually been retried already.

A failure after the request was sent can leave you not knowing whether it was written (a timeout, a dropped
connection). With supplied ids a retried create says so: ARX3001 for a record the first attempt wrote. With
generated ids it can't, so look the record up by a business key before writing it again. On MongoDB, a write outside a
transaction whose connection fails or times out after it was sent, or whose write concern fails, is ARX7005 (ARX7003
for a server time limit) with `IsTransient` false and a message that says its outcome is unknown (from
0.1.0-preview.5; it was transient): the server may still apply it, so a retry policy that follows `IsTransient` doesn't
apply it twice. A failure before anything was sent (no server reachable, a full pool) stays transient.

## A first connection

What a setup mistake looks like:

| Mistake | Error |
|---|---|
| Wrong user or password | ARX6001 PermissionDenied (the database's sign-in failure is the `InnerException`) |
| Wrong host or port, server not running | ARX7005 ConnectionFailure (transient) |
| No such database | ARX7005 ConnectionFailure on every SQL provider (PostgreSQL 3D000, SQL Server 4060, MySQL 1049), with `IsTransient` false: a retry won't make the database appear. Check `Database=` in the connection string. On MySQL a user without rights on every database gets ARX6001 PermissionDenied instead (MySQL error 1044, "access denied to database"): MySQL doesn't tell such a user whether the database exists, so check `Database=` as well as the grants |
| No such table | ARX5001 UndefinedEntity (from the database, on a read or a call without a catalog) |
| A table not declared in the schema catalog | ARX1016 IdNotDeclared, naming the line to add, and "Did you mean …?" when a declared name is close. A misspelt table name is the usual cause: the same typo is ARX1016 on a write with a catalog (refused before the database) and ARX5001 on a read (the database's answer) |
| A service without a catalog, on a table with no primary key (or a compound one) | ARX1019 TableNotResolvable, saying why and how to declare it |

Call `ValidateSchemaAsync()` once at startup: it connects and checks every declared table and id column against the
database, and says what doesn't match.
