Troubleshooting dynotable-orm: errors and common mistakes
What errors does dynotable-orm throw?
Four error classes, each thrown before any request is sent: ConfigError for a model definition that
cannot work, ValidationError for an item the schema rejects, ExpressionError for a request DynamoDB
would reject, and MarshallError for a value DynamoDB cannot store. Errors from DynamoDB itself are the
SDK's, re-thrown unchanged.
| error | means | thrown by |
|---|---|---|
ConfigError | a model definition that cannot work | model(), extend(), createTableInput(), extendZod(); a key or condition value whose field is async |
ValidationError | the schema rejected an item or a value | create, batchPut, transaction.create; update with the Zod adapter |
ExpressionError | a path, update, condition, key, query, batch or transaction DynamoDB would reject | every call that builds a request |
MarshallError | a value that cannot be stored or read | every call that writes or reads a value |
| the SDK's own errors | DynamoDB refused or failed the request | every call that sends a request |
All four classes are exported from dynotable-orm, so e instanceof ValidationError works. Each one's
name is its class name. The sections below go through the messages one by one: search this page for
the text of the message you see.
import {ConfigError, ExpressionError, MarshallError, ValidationError} from 'dynotable-orm';
export function describe(e: unknown): string {
if (e instanceof ConfigError) return `fix the model definition: ${e.message}`;
if (e instanceof ValidationError)
return `bad input at ${e.path.map(String).join('.')}: ${e.message}`;
if (e instanceof ExpressionError) return `bad request: ${e.message}`;
if (e instanceof MarshallError) return `cannot store a value at ${e.path.join('.')}: ${e.reason}`;
if (e instanceof Error) return `${e.name}: ${e.message}`; // from the AWS SDK
return String(e);
}Why does model() throw "the model has no hash key"?
ConfigError: the model has no hash key: set options.hashKey or mark a field. Every model must name its
hash key: mark a field with .hashKey() or hashKey(...), or pass the hashKey option. dynamoose took
the first attribute by default; dynotable-orm does not guess. See keys and
indexes.
Related messages from model():
two hash keys: 'id' (options.hashKey) and 'userId' (field): a mark and an option name different keys. Keep one.'status' has {...} in its field metadata but {...} in options.indexes: a mark and an option give the same setting with different values.the local index on 'placedAt' needs a table range key: a local index shares the table's hash key and sorts by its own field, so the table must have a range key.'meta.sk' is nested: only a top-level field can be a key, an index or a combine target: move the mark to a top-level field.the combine target 'k' is also one of its sources.
Why does model() say a field "is not declared in the schema"?
ConfigError: 'email' (index byEmail) is not declared in the schema. An option names a key, an index or
a combine field that the Zod schema does not declare. Zod strips undeclared keys, so the attribute would
be missing from every item. Add the field to the schema, or fix the name in the option.
Why does a timestamp field throw "must accept absence"?
ConfigError: the timestamp field 'createdAt' must accept absence or a Date: the lib sets its value.
With timestamps, the library sets createdAt and updatedAt itself. A required timestamp field must
take the value the library sets: a Date, or a number for a number field. Declare it as z.date() or
z.date().optional(), not as a string or with a check a Date fails.
the timestamp field 'createdAt' is a number, always stored in milliseconds: remove its storage setting: a number timestamp holds epoch milliseconds, so it takes no storage. See
timestamps.
Why does createTableInput() say it "cannot tell whether" a key is a string or a number?
ConfigError: createTableInput() cannot tell whether 'id' is a string or a number from the schema.
AttributeDefinitions needs S or N for each key attribute, and the schema does not give exactly one.
This happens for every non-Date key of a schema that is not Zod, and for a Zod key such as a union of a
string and a number. Write the CreateTable input yourself, or give the key one type.
What does "a key or condition value needs a sync one" mean?
ConfigError: 'id' has an async check or transform, or one that throws; a key or condition value needs a sync one. With the Zod adapter, a key or condition value goes through its field's transforms, and that
parse is synchronous. A field with .refine(async …) or an async transform can be validated on a
whole-item write, but it cannot be a key or a condition value. Make that field's checks synchronous, or
pass {rawKey: true} to send a key as given.
Why does extend() say it "would replace" a method?
ConfigError: extend() on 'User' would replace get. extend() refuses a name that the model already has:
get, query, create and the other built-in methods. Rename the method. A method that an earlier
extend() added can be replaced. Methods of your own
covers extend().
ConfigError: extendZod: ZodType already has a different 'storage' method: another Zod extension
already added a method with that name. Use the function form of the marks instead (storage(z.date(), 'seconds')), which changes no prototype.
What does a ValidationError tell you?
The schema rejected an item, or one value of an update. The message is the schema's message for the first
issue, plus its path: Too small: expected string to have >=1 characters at title. A value outside an
enum and a value of the wrong type get dynamoose's wording instead:
status must equal ["A","B"], but is set to C and
Expected n to be of type number, instead found type string.
e.issues: every issue the schema reported, each with amessageand apath.e.path: the path of the first issue, such as['title']or['lines', 0, 'sku'].
Common causes:
- Old data written again. Every whole-item write runs the full schema, refinements and formats
included. An item that dynamoose stored can fail when you read it and write it back. Fix the data, or
loosen the schema: for example
z.guid()for ids that are not RFC UUIDs. - An array for a Set. A
z.set()field takes only aSetoncreateand$SET. Passnew Set(array).$ADDand$DELETEstill take an array. - A
$REMOVEof a required field. With the Zod adapter, removing a field that the schema requires fails, and so does setting it toundefined. - An undeclared key on a strict object.
z.strictObject()rejects unknown keys:Unrecognized key: "x". - A required field missing on
create. The write type is aPartialof the schema's input, so the compiler lets a missing field through and the runtime check catches it.
Schema validation lists what is checked on each call.
Why does an update throw "the update has nothing to write"?
ExpressionError: the update has nothing to write. The changes object holds no change that DynamoDB
could apply. dynamoose sends such an update, and DynamoDB rejects it. Two common causes:
- The changes are empty:
update(key, {})on a model without timestamps. - Every field in the changes is undeclared. A top-level
$SETof an attribute the schema does not declare is dropped, so nothing is left. Check the field names, or declare the field. To write an undeclared attribute on purpose, pass{allowUndeclared: true}toupdate.
Nothing to update: the schema does not declare meta.x. is the same case for a nested path.
Why does an update throw "a path that overlaps it"?
ExpressionError: the update writes 'tags' and a path that overlaps it: DynamoDB rejects that. One update
writes a path twice, or a path and a path inside it: $ADD and $DELETE on the same Set, address and
address.city. Split it into two updates, or merge the changes. An updatedAt in the changes of a
timestamped model does not throw: the library ignores it and writes its own stamp.
Why does an update say a field "cannot be updated alone"?
ExpressionError: period.from cannot be updated alone: period has its own checks or transform, so write the whole period. The parent object has a refinement or a transform, which can only judge a whole value. A
write of one field below it would skip that rule. Update the whole parent:
update(key, {period: {from, to}}).
Related: You must update all or none of the combine attributes when running Model.update, for an update
that writes only some sources of a combined field, and You cannot update the combine attributes of sk because it is part of the key, for sources of a combined key.
Why does a query throw "no index fits the query"?
ExpressionError: no index fits the query: it needs an equality on a hash key. A DynamoDB query must name
one hash key value, of the table or of an index. Add an eq() on a hash key at the top level of the
chain, not under an or(). To read every item, use scan(). See query and scan.
Related messages:
the query needs an equality on the hash key 'author':using()names an index whose hash key has no equality in the conditions.'byOwner' is not an index of the model:using()names an undeclared index. It does not compile either, unless the types were bypassed.a scan has no sort order,only a scan runs in parallel segments, andstartAt() takes one key.
Why does a call throw "the key of … has no …"?
ExpressionError: the key of 'Message' has no 'sentAt'. The key object lacks a key attribute, or holds
undefined or null for one. A model with a range key needs both: {roomId, sentAt}. A bare value is a
key only for a model with a hash key alone.
the set hook of 'User' removed the key attribute 'id': the model's set option dropped a key attribute
from the key it got.
Why does a condition throw "was passed undefined" or "has no comparison"?
eq() on 'status' was passed undefined: a comparison gotundefined, often a variable that was never set. DynamoDB would reject the expression. To test for a missing attribute, useattributeNotExists().the condition on 'status' has no comparison: awhere()with nothing after it.beginsWith() cannot follow not():beginsWith,inandbetweencannot be negated.in() on 'status' takes an array of values, andan "in" condition needs at least one value.{gt, lt} on 'points' names 2 comparisons: give one, or chain them: the object form takes one comparison per attribute.'nope' is not an attribute of the model: an untypednew Condition('nope')names an undeclared attribute.a group needs at least one condition: an emptygroup().
Conditions covers the chain.
Why do a transaction or a batch throw before sending?
a transaction holds 1 to 100 items, not 0: DynamoDB takes 1 to 100 actions per transaction. Split a larger set into several transactions, or use a batch when the writes need not happen together.the transaction writes the key [...] twice: two entries on one item. DynamoDB rejects that too: merge them into one entry.transaction.condition() on 'Stock' needs a condition: aConditionCheckentry without a condition.the batch holds the key [...] twice: a batch names one key twice. Remove the duplicate.
What does a MarshallError mean?
A value has no DynamoDB form. The message ends with the path of the value, and e.path and e.reason
hold the two parts:
an empty Set cannot be stored at tags: DynamoDB has no empty Set. Leave the attribute out, or remove it.a Set cannot mix typesanda Set can hold only strings, numbers or bytes.NaN is not a finite number within the safe integer range at score:NaN,Infinityand integers beyondNumber.MAX_SAFE_INTEGERcannot be stored as a number without losing precision. Use abigintfor a large integer: it is written as a number, and a large stored integer reads back as abigint.a Map instance cannot be stored: aMapor another class instance. Convert it to a plain object first. A function givesa value of type function cannot be stored.an invalid Date cannot be stored: anew Date('not a date').
With the Zod adapter, the schema usually rejects these values first, with a ValidationError. A
MarshallError then means a value that no schema checks, such as an $ADD delta.
How do you catch ConditionalCheckFailedException?
With isConditionalCheckFailed(e). A create on a stored key, or a write whose condition fails, makes
DynamoDB refuse the write, and the SDK throws a ConditionalCheckFailedException:
import {isConditionalCheckFailed, model} from 'dynotable-orm';
import {hashKey} from 'dynotable-orm/zod';
import {z} from 'zod';
const User = model('User', z.object({id: hashKey(z.string()), name: z.string()}));
try {
await User.create({id: 'u1', name: 'Ada'});
} catch (e) {
if (!isConditionalCheckFailed(e)) throw e;
console.log('u1 already exists');
}It matches the error by its name, so it works with any copy of the SDK. A test of
e.code === 'ConditionalCheckFailedException' from dynamoose code never matches: SDK v3 errors have no
code.
Why does a transaction throw TransactionCanceledException?
DynamoDB cancelled the whole transaction: a condition failed, or another request was writing one of the
items. isConditionalCheckFailed is false for it. Read the reasons array that transaction() adds to
the error: one code per entry, in order, such as ['ConditionalCheckFailed', 'None']. A
'TransactionConflict' reason can be retried. See when a transaction
fails.
Why does DynamoDB throw ResourceNotFoundException?
The table does not exist under the name the model uses. A model's table name is
${prefix}${name}${suffix}, so check Model.tableName, the prefix and suffix from setDefaults(),
and the region and endpoint of the client. dynotable-orm never creates a table: create it from
createTableInput() or in your infrastructure code. See creating the
table.
Mistakes that throw nothing
- Dates read back as strings. A model on a table that holds ISO strings, without
defaultDateStorage: 'iso', writes epoch milliseconds next to the old strings, and old items read back with a string where the type saysDate. Read the Date storage trap. - An update created an item. DynamoDB's
UpdateItemcreates a missing item with only the updated attributes. Add{condition: M.condition().where('id').attributeExists()}. - A field disappears.
z.object()drops undeclared keys on write and on read. Declare the field, or usez.looseObject(). See unknown keys. - A query reads another index than you meant. Without
using(), the library picks one from the conditions. Name it withusing(). - A page holds fewer items than
limit(), or none. DynamoDB appliesLimitbefore the filter. Loop onlastKey. See pagination. - A
not()has no effect.not()negates the next comparison, andwhere()starts a new one, so writewhere('status').not().eq('done'), notnot().where('status').eq('done'). - A transaction ignores the model's client.
transaction()uses its ownclientoption, elsesetClient(). Pass{client}totransaction(). - A cancelled transaction slips past
isConditionalCheckFailed. Readreasonsinstead.
Next steps
- The error classes in the reference:
ConfigError,ExpressionError,ValidationErrorandMarshallError. - Stricter checks: where dynotable-orm throws and dynamoose did not.