Skip to content

Error Codes

Error Codes

Every public function returns TDB_SUCCESS (0) or one of these. They are negative, so rc < 0 tests for failure and rc != TDB_SUCCESS is equivalent.

CodeValueMeaning
TDB_SUCCESS0Success
TDB_ERR_MEMORY-1Allocation failed
TDB_ERR_INVALID_ARGS-2A NULL or out-of-range argument, or a configuration that failed validation
TDB_ERR_NOT_FOUND-3The key, column family, or savepoint does not exist
TDB_ERR_IO-4A read, write, or fsync failed
TDB_ERR_CORRUPTION-5On-disk data did not decode
TDB_ERR_EXISTS-6A column family with that name already exists
TDB_ERR_CONFLICT-7Another transaction committed a conflicting write first
TDB_ERR_TOO_LARGE-8A value does not fit the space that holds it — a buffer too small, or a database that has used every column family id
TDB_ERR_MEMORY_LIMIT-9A configured memory limit was reached
TDB_ERR_INVALID_DB-10The database is closing or otherwise unusable
TDB_ERR_UNKNOWN-11An error with no more specific code
TDB_ERR_LOCKED-12Transient contention — retry
TDB_ERR_READONLY-13A write was attempted against a read-only database
TDB_ERR_TXN_EXPIRED-14The transaction is no longer active
TDB_ERR_NO_SPACE-15The device or filesystem is out of space
TDB_ERR_TXN_ABORTED-16Another thread aborted the transaction through tidesdb_txn_request_abort
TDB_ERR_TOO_OLD-17The sequence asked for is older than the oldest reconstructable point in time

The two that mean “ask again”

Neither is a failure. Both say the operation did not happen and could succeed if repeated, and the damaging mistake is to record either as a permanent error — above all to mistake one for an absent key.

TDB_ERR_CONFLICT

From any level above TDB_ISOLATION_READ_COMMITTED, and only from commit or prepare. Nothing durable was written; the transaction is aborted.

What was violated depends on the level, and the two causes are different events. At TDB_ISOLATION_REPEATABLE_READ a key you read gained a newer committed version. At TDB_ISOLATION_SNAPSHOT a key you wrote was written by someone who committed first. TDB_ISOLATION_SERIALIZABLE reports either, and additionally the write-skew check.

Retry the whole transaction — begin again, redo the reads, redo the writes. Retrying just the commit is meaningless, since the reads it was validated against are stale.

TDB_ERR_LOCKED

Contention the engine could not absorb on your behalf. Where you meet it depends on what you asked for, and the two cases are not equally common.

From a maintenance calltidesdb_compact, tidesdb_compact_range, tidesdb_cf_update_runtime_config, tidesdb_rename_column_family, tidesdb_clone_column_family, tidesdb_backup — it means another exclusive operation already holds that family. tidesdb_flush_memtable, and tidesdb_checkpoint through it, report it for a related but distinct reason: the immutable queue had not drained when their bounded wait expired, which says flush is behind rather than that anything holds a family. This is the ordinary case. tidesdb_compact, tidesdb_compact_range, tidesdb_cf_update_runtime_config and tidesdb_backup report it immediately rather than park you behind a compaction that could run for minutes. tidesdb_rename_column_family and tidesdb_clone_column_family differ: they wait out a bounded quiesce window — a minute — and report locked only if the family was still held at the end of it, because both have to reach a moment when nothing else is working on the family. Either way, retry later, or don’t; the work you asked for is in many cases already happening.

From a read or a scan it is rare. Transient pressure — a memtable rotating out from under a reader, a momentarily exhausted descriptor budget — is waited out inside the engine, because a caller told “try again” has no lever the engine does not already have. Reaching a caller means the pressure outlasted every internal recheck.

It never means “not found”. A read returning TDB_ERR_LOCKED is saying the data may well exist but could not be reached right now. Code that folds it into an absence produces silent wrong answers under load — the single most common way to misuse this API.

Retry with a short backoff.

Which functions produce what

CodeTypically from
TDB_ERR_CONFLICTtidesdb_txn_commit, tidesdb_txn_prepare, at any level above TDB_ISOLATION_READ_COMMITTED
TDB_ERR_LOCKEDtidesdb_compact, tidesdb_compact_range, tidesdb_cf_update_runtime_config, tidesdb_rename_column_family, tidesdb_clone_column_family, tidesdb_backup; tidesdb_flush_memtable and tidesdb_checkpoint when the immutable queue does not drain in time; tidesdb_range_stats when the layout moves mid-scan; rarely, reads, iterator steps and tidesdb_iter_new under pressure the engine could not absorb
TDB_ERR_NOT_FOUNDtidesdb_txn_get, tidesdb_txn_contains, family and savepoint lookups
TDB_ERR_EXISTStidesdb_create_column_family, tidesdb_clone_column_family
TDB_ERR_TXN_EXPIREDThe first operation on a transaction whose timeout has passed. Only when a timeout was set, by txn_timeout_seconds or tidesdb_txn_set_timeout; it is reported once and the transaction is then aborted
TDB_ERR_TXN_ABORTEDEvery operation on a transaction after another thread called tidesdb_txn_request_abort on it, the commit included. Distinct from TDB_ERR_CONFLICT on purpose: a conflict is the engine’s own verdict between two writers, where this says an outside authority ruled and the engine never weighed in
TDB_ERR_TOO_OLDtidesdb_txn_begin_at_seq for a sequence a collection has already run below. It is not a transient condition and retrying never clears it — the versions that point named are gone, and the boundary only moves further away. Hold a snapshot in advance for a point in time you know you will want
TDB_ERR_INVALID_DBOperations against a closing database
TDB_ERR_TOO_LARGEtidesdb_recover_prepared when the buffer is too small, and tidesdb_create_column_family when the family id space is spent
TDB_ERR_CORRUPTIONOpen and read paths, on undecodable on-disk data
TDB_ERR_NO_SPACEtidesdb_flush_memtable, tidesdb_checkpoint, and any path that writes an sstable

TDB_ERR_NO_SPACE

The device or filesystem could not take the write. It is separated from TDB_ERR_IO because it is the one write failure that says what to do about it: nothing is damaged, and the operation succeeds once space is freed. A generic I/O error carries no such promise, and treating a full disk as one is how a filled device gets mistaken for a defect in the engine.

Quota exhaustion (EDQUOT, where the platform defines it) reports the same code, since it is the same condition and the same remedy.

Data already committed is unaffected. What fails is the attempt to write new files — a flush, a compaction, a checkpoint — so the memtable and its log keep the data until a flush can land it. Free space and retry.

The engine also logs the condition once per failing file, with the remaining free space, so it is distinguishable in a log without reproducing it.

Turning a code into text

const char *tidesdb_strerror(int code);

Returns a short description of any result code, for a log line or an error message. The string is a static literal: never NULL, never freed, valid for the life of the process. An unrecognised code describes itself as unknown rather than returning NULL, so a result can be passed straight through without being checked first.

int rc = tidesdb_txn_commit(txn);
if (rc != TDB_SUCCESS) fprintf(stderr, "commit failed, %s\n", tidesdb_strerror(rc));

It describes the code, not the cause. Which key, which family, and which file are yours to add.

Handling pattern

int rc;
int attempts = 0;
do {
rc = tidesdb_txn_get(txn, cf, key, key_size, &value, &value_size);
} while (rc == TDB_ERR_LOCKED && ++attempts < MAX_RETRIES);
if (rc == TDB_SUCCESS)
{
/* use value, then free it */
tidesdb_free(value);
}
else if (rc == TDB_ERR_NOT_FOUND)
{
/* genuinely absent */
}
else
{
/* a real failure */
}

Note the three-way split. Collapsing “absent” and “failed” into one branch is the shape that hides TDB_ERR_LOCKED.

On TDB_ERR_CORRUPTION

It means bytes did not decode — a damaged file, a truncated record, a foreign file in the directory. It does not always mean data loss:

  • A damaged manifest is discarded and rebuilt from the sstables on disk, and does not surface as an error at all — though a rebuilt family comes back named cf_ followed by its zero-padded id, with the default configuration.
  • A torn final record in a log is discarded during recovery; it was never acknowledged.
  • A corrupt sstable block is a genuine problem: restore from a backup.

If corruption appears on a healthy disk, suspect the storage stack before the engine, and check whether anything else is writing into the database directory.