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.
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(){
"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.
import { createNarsil, isNarsilError } from '@delali/narsil'
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' })
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))
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')
console.log(JSON.stringify({ outcomes, commuterPrice: commuter?.price }, null, 2))
await narsil.shutdown(){
"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.
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')
console.log(JSON.stringify({ outcomes, commuterPrice: commuter?.price }, null, 2))
const imported = await narsil.insertBatch('products', [
{ 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(){
"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, while the client repeats the duplicate insert. Install uWebSockets.js first, as the HTTP server page describes.
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(){
"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
valueswhen 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 covers the writes behind most of these codes.
- Filters and facets covers the filter operators that each field type accepts.
- HTTP server covers the server that turns each code into a status.