> Complete content index: https://narsil.sondelali.com/llms.txt

# Errors

> Read the code on a Narsil failure, branch on it from any entry point, and make a strict index reject a query that names a misspelt field.

URL: https://narsil.sondelali.com/docs/errors
Section: Core concepts
Version: 0.3 (@delali/narsil@0.3.0, latest)

Narsil raises a failure as a `NarsilError`, which holds a `code` that you branch on, a `message` for a log or a person, and `details` with the values behind the failure. You build `catalogue.ts` here, in which you read one failure, recover from the one that you expect, and collect the failures from a batch. You then build `remote.ts`, in which your code catches the same failure from a server through the HTTP client. Each code block below holds the whole file, with green lines marking the lines that you add at each step and struck lines marking the lines that you remove.

## Read what went wrong

An insert under an id that the index already holds fails with `DOC_ALREADY_EXISTS`. The program catches that error and, once `isNarsilError` returns true for it, prints its three fields.

```ts title="catalogue.ts"
import { createNarsil, isNarsilError } from '@delali/narsil'

const narsil = await createNarsil()

await narsil.createIndex('products', {
  schema: { name: 'string', price: 'number', category: 'enum' },
  required: ['price'],
})
await narsil.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 740, category: 'city' })

try {
  await narsil.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 760, category: 'city' })
} catch (error) {
  if (!isNarsilError(error)) throw error
  console.log(JSON.stringify({ code: error.code, message: error.message, details: error.details }, null, 2))
}

await narsil.shutdown()
```

```json result open
{
  "code": "DOC_ALREADY_EXISTS",
  "message": "Document \"urban-commuter\" already exists in partition 0",
  "details": {
    "docId": "urban-commuter",
    "partitionId": 0
  }
}
```

`details` holds the id and the number of the partition that stores it, so you can log both without parsing the message. Branch on `code` and never on `message`, because each code stays the same across releases, while the wording of a message can change. `isNarsilError` returns false for any error that Narsil did not raise, such as a `TypeError` from your own code inside the same `try`, so throw such an error on unchanged, as the program does.

## Recover from the failure that you expect

`ErrorCodes` holds every code that the engine raises as a constant, so your editor completes each name and TypeScript rejects a misspelt one. The engine has no upsert, so `saveProduct` below inserts a product, updates it where the insert fails with `DOC_ALREADY_EXISTS`, and throws every other error on to its caller.

```ts title="catalogue.ts"
import { createNarsil, isNarsilError } from '@delali/narsil' // [!code --]
import { createNarsil, ErrorCodes, isNarsilError } from '@delali/narsil' // [!code ++]

const narsil = await createNarsil()

await narsil.createIndex('products', {
  schema: { name: 'string', price: 'number', category: 'enum' },
  required: ['price'],
})
await narsil.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 740, category: 'city' })

try { // [!code --]
  await narsil.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 760, category: 'city' }) // [!code --]
} catch (error) { // [!code --]
  if (!isNarsilError(error)) throw error // [!code --]
  console.log(JSON.stringify({ code: error.code, message: error.message, details: error.details }, null, 2)) // [!code --]
interface Product { // [!code ++:5]
  id: string
  name: string
  price: number
  category: string
}

const saveProduct = async (product: Product): Promise<'inserted' | 'updated'> => { // [!code ++:18]
  try {
    await narsil.insert('products', { ...product })
    return 'inserted'
  } catch (error) {
    if (!isNarsilError(error) || error.code !== ErrorCodes.DOC_ALREADY_EXISTS) throw error
    await narsil.update('products', product.id, { ...product })
    return 'updated'
  }
}

const outcomes = [
  await saveProduct({ id: 'cargo-hauler', name: 'Walsh cargo hauler', price: 1890, category: 'cargo' }),
  await saveProduct({ id: 'urban-commuter', name: 'Gomes Urban commuter', price: 760, category: 'city' }),
]
const commuter = await narsil.get('products', 'urban-commuter')

console.log(JSON.stringify({ outcomes, commuterPrice: commuter?.price }, null, 2))

await narsil.shutdown()
```

```json result open
{
  "outcomes": [
    "inserted",
    "updated"
  ],
  "commuterPrice": 760
}
```

`saveProduct` stores the cargo hauler on the first call. On the second call, however, the insert fails with `DOC_ALREADY_EXISTS`, so `saveProduct` replaces the stored commuter, whose price moves from 740 to 760. `saveProduct` passes every other failure on to its caller, so saving a product whose `price` holds text from an import still fails with `DOC_VALIDATION_FAILED`. `update` checks `required` as `insert` does, which means that a product without a `price` fails with `DOC_MISSING_REQUIRED_FIELD` on either path through `saveProduct`.

## Collect the failures from a batch

`insertBatch` throws nothing for a document that fails, because it stores every document that it can and lists each failure with the id of its document. Read `failed` after every batch, because `insertBatch` resolves even where it stores nothing.

```ts title="catalogue.ts"
import { createNarsil, ErrorCodes, isNarsilError } from '@delali/narsil'

const narsil = await createNarsil()

await narsil.createIndex('products', {
  schema: { name: 'string', price: 'number', category: 'enum' },
  required: ['price'],
})
await narsil.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 740, category: 'city' })

interface Product {
  id: string
  name: string
  price: number
  category: string
}

const saveProduct = async (product: Product): Promise<'inserted' | 'updated'> => {
  try {
    await narsil.insert('products', { ...product })
    return 'inserted'
  } catch (error) {
    if (!isNarsilError(error) || error.code !== ErrorCodes.DOC_ALREADY_EXISTS) throw error
    await narsil.update('products', product.id, { ...product })
    return 'updated'
  }
}

const outcomes = [
  await saveProduct({ id: 'cargo-hauler', name: 'Walsh cargo hauler', price: 1890, category: 'cargo' }),
  await saveProduct({ id: 'urban-commuter', name: 'Gomes Urban commuter', price: 760, category: 'city' }),
]
const commuter = await narsil.get('products', 'urban-commuter') // [!code --]

console.log(JSON.stringify({ outcomes, commuterPrice: commuter?.price }, null, 2)) // [!code --]
const imported = await narsil.insertBatch('products', [ // [!code ++:17]
  { id: 'gravel-tourer', name: 'Larsson gravel tourer', price: 1340, category: 'gravel' },
  { id: 'cargo-hauler', name: 'Walsh cargo hauler', price: 1890, category: 'cargo' },
  { id: 'folding-city', name: 'Mori folding city bike', category: 'city' },
])

console.log(
  JSON.stringify(
    {
      outcomes,
      stored: imported.succeeded,
      failed: imported.failed.map(({ docId, error }) => ({ docId, code: error.code, details: error.details })),
    },
    null,
    2
  )
)

await narsil.shutdown()
```

```json result open
{
  "outcomes": [
    "inserted",
    "updated"
  ],
  "stored": [
    "gravel-tourer"
  ],
  "failed": [
    {
      "docId": "folding-city",
      "code": "DOC_MISSING_REQUIRED_FIELD",
      "details": {
        "field": "price"
      }
    },
    {
      "docId": "cargo-hauler",
      "code": "DOC_ALREADY_EXISTS",
      "details": {
        "docId": "cargo-hauler",
        "partitionId": 0
      }
    }
  ]
}
```

The engine stores the gravel tourer and reports the other two documents. The engine keeps its own order in `failed`, where the missing price comes ahead of the duplicate although the program sends the duplicate first. Match each failure to its document by `docId` for that reason. Each `error` in that list is a `NarsilError`, which holds the same fields as the error that a single insert throws.

## Catch an error from any entry point

Each of the entry points `@delali/narsil`, `@delali/narsil/client`, and `@delali/narsil/react` holds its own copy of the `NarsilError` class, so an `instanceof` test against one copy returns false for an error that another copy built. `isNarsilError` tests for a marker that the constructor of every copy writes into the error, which is why it returns true for an error from any entry point. In `remote.ts`, the engine serves the catalogue through the [HTTP server](/docs/http-server), while the client repeats the duplicate insert. Install `uWebSockets.js` first, as the HTTP server page describes.

```ts title="remote.ts"
import { createNarsil, NarsilError } from '@delali/narsil'
import { createNarsilClient, isNarsilError } from '@delali/narsil/client'
import { createServer } from '@delali/narsil/server'

const engine = await createNarsil()

await engine.createIndex('products', { schema: { name: 'string', price: 'number', category: 'enum' } })
await engine.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 740, category: 'city' })

const server = createServer(engine, { host: '127.0.0.1', port: 7700 })

await server.listen()

const client = createNarsilClient({ url: 'http://127.0.0.1:7700' })

try {
  await client.insert('products', { id: 'urban-commuter', name: 'Gomes Urban commuter', price: 760, category: 'city' })
} catch (error) {
  if (!isNarsilError(error)) throw error
  console.log(
    JSON.stringify({ instanceOfEngineClass: error instanceof NarsilError, code: error.code, details: error.details }, null, 2)
  )
}

await server.shutdown()
```

```json result open
{
  "instanceOfEngineClass": false,
  "code": "DOC_ALREADY_EXISTS",
  "details": {
    "docId": "urban-commuter",
    "partitionId": 0,
    "status": 409
  }
}
```

The client throws the code that the server sent and adds the HTTP status, 409 here, to `details`. The `instanceof` test against the class from `@delali/narsil` returns false for that error, although `isNarsilError` returned true for it a line earlier. A failure that the server raises before the request reaches the engine, such as a body over the size limit, holds a code from `ServerErrorCodes`. A failure on the caller's side holds a code from `ClientErrorCodes`, such as `CLIENT_CONNECTION_FAILED` when no server listens at the address.

## See the codes and their HTTP statuses

The table lists the codes that these pages cover, with the status that the HTTP server answers each one with, while `ErrorCodes` holds every other code.

| Code | Cause | Status |
| --- | --- | --- |
| `DOC_ALREADY_EXISTS` | You insert a document under an id that the index already holds. | 409 |
| `DOC_NOT_FOUND` | You update or remove a document under an id that the index lacks. | 404 |
| `DOC_VALIDATION_FAILED` | You write a field value of the wrong type, or pass a `docId` that contradicts the document's own `id`. | 400 |
| `DOC_MISSING_REQUIRED_FIELD` | You insert or update a document without a field that `required` names. | 400 |
| `INDEX_NOT_FOUND` | You name an index that the engine does not hold. | 404 |
| `INDEX_ALREADY_EXISTS` | You create an index under a name that the engine already holds. | 409 |
| `SCHEMA_INVALID_TYPE` | You give a schema field a type that Narsil does not support. | 400 |
| `SCHEMA_DEPTH_EXCEEDED` | You nest schema objects five levels deep. | 400 |
| `SEARCH_INVALID_FIELD` | You name a field in `fields` that holds no text, boost a field that the schema lacks, sort on a plain `string` or a `geopoint` field, or query a strict index on a field that its schema lacks. | 400 |
| `SEARCH_INVALID_FILTER` | You filter with an operator or a unit that Narsil does not support, with `startsWith` on a `number` field, with `between` and one bound, with a negative `radius`, or with a polygon of fewer than three points. | 400 |
| `SEARCH_INVALID_MODE` | You name a search mode that Narsil does not support, set a `sort` on a hybrid query, or sort in a direction other than `asc` and `desc`. | 400 |
| `SEARCH_INVALID_CURSOR` | You pass `listDocuments` a cursor that the engine did not issue. | 400 |
| `SEARCH_RESULT_WINDOW_EXCEEDED` | You ask through `offset` and `limit` for results past the first 10,000. | 400 |
| `PARTITION_CAPACITY_EXCEEDED` | You write past the capacity of an index's partitions, or rebalance past `maxPartitions`. | 409 |
| `CONFIG_INVALID` | You set an option to a value that the engine does not accept, or name an option that the engine lacks. | 400 |
| `EMBEDDING_FAILED` | Your embedding adapter fails to embed a query. | 503 |
| `PERSISTENCE_SAVE_FAILED` | You write or checkpoint after the durability directory has gone missing, or checkpoint into a folder that the process cannot write to. | 503 |
| `INSTANCE_SHUT_DOWN` | You call the engine after `shutdown` has started. | 503 |

The server answers a code that its mapping lacks with 500. It also replaces any error that Narsil did not raise with `INTERNAL_ERROR` and a generic message, so that no stack trace reaches the caller.

## Turn a misspelt field name into an error

On an index without `strict`, you may name a field outside the schema in a filter, a sort, a facet, or a group, in which case the engine answers from the values that your documents store under that name. A misspelt field name therefore raises no error, because the engine treats it as a field that no document stores:

- A filter on that field matches nothing.
- The engine leaves the hits in id order under a sort on that field, whichever direction you give.
- The engine returns a facet on that field with no values and a count of 0.
- The engine puts every hit in one group with empty `values` when you group on that field.

The engine refuses every document that holds a field outside the schema of an index created with `strict: true`, so on that index it throws `SEARCH_INVALID_FIELD` for a filter, a sort, a facet, or a group that names such a field. A misspelling in any of those four then fails on the query that holds it, as a misspelling in `fields` or in `boost` does on every index.

## Where to go next

- [Indexes and documents](/docs/indexes-and-documents) covers the writes behind most of these codes.
- [Filters and facets](/docs/filters-and-facets) covers the filter operators that each field type accepts.
- [HTTP server](/docs/http-server) covers the server that turns each code into a status.
