Skip to content

Error handling

Table operations return Result values. Most use slate.DetsError for errors. slate/set.update_counter uses set.UpdateCounterError instead. with_exclusive_table uses exclusive.SessionError; its callback still uses DetsError. The helpers slate.error_code and slate.error_message return String values.

Callbacks passed to with_table, fold, or fold_results can raise exceptions. Those exceptions propagate to the caller. with_table attempts to close the table before it re-raises a callback exception.

For a stable machine-readable code or a user-facing message, use slate.error_code and slate.error_message.

Match the errors that your application expects. Handle or return other errors:

import gleam/io
import slate
import slate/set
case set.lookup(table, key: "config") {
Ok(value) -> io.println("Found: " <> value)
Error(slate.NotFound) -> io.println("Configuration key not found")
Error(other) ->
io.println("[" <> slate.error_code(other) <> "] " <> slate.error_message(other))
}

let assert panics if the result does not match the pattern. Use it in tests or when a failure should stop the current process. Use pattern matching or result.try when the caller must handle the error.

let assert Ok(table) = set.open("data/cache.dets",
key_decoder: decode.string, value_decoder: decode.string)
let assert Ok(Nil) = set.insert(table, "key", "value")
let assert Ok(Nil) = set.close(table)

set.lookup returns this error when the key does not exist. Bag and duplicate bag tables return Ok([]) instead. See Lookup behavior differences.

import gleam/dynamic/decode
import slate
import slate/set
let assert Ok(table) = set.open("data/users.dets",
key_decoder: decode.string, value_decoder: decode.int)
let assert Error(slate.NotFound) = set.lookup(table, key: "nonexistent")
let assert Ok(Nil) = set.close(table)

set.insert_new returns this error when the key exists. bag.insert_new returns it when the exact entry already exists. Plain insert does not return this error. It replaces the value in a set or ignores a duplicate entry in a bag.

let assert Ok(Nil) = set.insert_new(table, "alice", 42)
let assert Error(slate.KeyAlreadyPresent) = set.insert_new(table, "alice", 99)

This error indicates denied file access or an attempted write to a table opened with ReadOnly access. Inspect the context to identify the cause.

import gleam/dynamic/decode
import slate.{AutoRepair, ReadOnly}
import slate/set
let assert Ok(table) = set.open_with_access(path: "data/users.dets",
repair: AutoRepair, access: ReadOnly,
key_decoder: decode.string, value_decoder: decode.string)
let assert Error(slate.AccessDenied(context)) = set.insert(table, "key", "value")
let assert Ok(Nil) = set.close(table)

Opening a DETS file with the wrong table type or key position returns this error. For example, you cannot open a set file as a bag.

import gleam/dynamic/decode
import slate
import slate/bag
import slate/set
// Create a set table
let assert Ok(table) = set.open("data/store.dets",
key_decoder: decode.string, value_decoder: decode.string)
let assert Ok(Nil) = set.insert(table, "key", "value")
let assert Ok(Nil) = set.close(table)
// Opening the same file as a bag fails
let assert Error(slate.TypeMismatch(context)) = bag.open("data/store.dets",
key_decoder: decode.string, value_decoder: decode.string)

The file or a parent directory is missing. Returned by open* or is_dets_file for OTP's enoent reason. Permission failures use AccessDenied.

The path exists and is readable, but the file is not a valid DETS file. slate.is_dets_file can identify DETS files before you open them. It does not guarantee integrity or safe access to untrusted paths.

The file needs repair, and you opened it with NoRepair. Back up the closed file before you attempt repair with AutoRepair or ForceRepair. Repair does not guarantee full data recovery. See Repair policies.

The table is already open with an incompatible configuration (for example, different access mode). Match AlreadyOpen(context) to inspect the path and the "incompatible_arguments" reason.

The table handle is no longer valid, usually because the table is already closed.

A write would push the DETS file past its 2 GB hard limit. See Limitations for context and mitigations.

slate uses a bounded internal pool of DETS table name slots. If too many distinct files are open at once, new opens fail with this error. See Troubleshooting for recovery steps.

The data on disk did not match the decoders provided when opening the table. See DecodeErrors for diagnosis and recovery strategies.

This variant contains an unexpected Erlang-level error. The string is for diagnostics only and is not a stable API. Use slate.error_code(err), which returns "unexpected_error", for programmatic matching. Report a GitHub issue if you encounter one.

update_counter returns Result(Int, set.UpdateCounterError). This type adds CounterValueNotInteger without adding a counter-specific variant to DetsError:

import gleam/int
import gleam/io
import slate
import slate/set
case set.update_counter(table, key: "hits", increment: 1) {
Ok(new_value) -> io.println("Hits: " <> int.to_string(new_value))
Error(set.CounterValueNotInteger) -> io.println("The counter value is not an integer")
Error(set.TableError(slate.NotFound)) -> io.println("The counter key does not exist")
Error(set.TableError(err)) -> io.println(slate.error_message(err))
}

with_exclusive_table returns Result(a, exclusive.SessionError). Match AcquisitionFailed(Busy) and AcquisitionFailed(TimedOut) when your application can skip a cache operation after a warning. Other acquisition errors include invalid waits, unsupported targets or filesystems, missing native code, unsafe aliases, and OS failures. The callback does not run after acquisition fails.

SessionFailed(OpenFailure(error)) and SessionFailed(CallbackFailure(error)) contain a DetsError. A callback error does not undo earlier writes.

CleanupFailed(primary, failures) retains a primary failure when present, plus close, release, or guardian failures. A late acquisition that also fails release retains Some(AcquisitionFailure(TimedOut)). Treat cleanup failure as a quarantine condition: ownership can remain until the VM exits. Do not delete the sidecar to clear it.

Use exclusive.error_code and exclusive.error_message, rather than slate.error_code and slate.error_message, for session errors. The safe classifiers omit paths and native diagnostic details. The cleanup code is "exclusive_cleanup_failed".

Slate re-raises callback exceptions after cleanup. It sends cleanup failures to Erlang Logger on exception or caller-death paths that cannot return a normal result. See exclusive sessions for ownership requirements and recovery.

FileNotFound, AlreadyOpen, AccessDenied, TypeMismatch, NeedsRepair, NotADetsFile, and FileSizeLimitExceeded each contain FileErrorContext(path: Option(String), reason: String).

The path is the filename that OTP reports. If OTP returns AlreadyOpen without a path, open operations retain the path they passed to OTP. Open operations normally report an absolute path. is_dets_file can report a relative path.

None means there is no single filename. Do not substitute an empty string or guess from a table-name atom. For rename failures, reason retains both filenames and path is None. The context remains available after the table closes.

The reason retains diagnostic details such as "enoent", "{error,eacces}", "access_mode", or "keypos_mismatch". Do not parse this text to identify the error category. Paths and reasons can contain sensitive data. Use them only in trusted logs. error_code and error_message omit this context.

An error does not guarantee that a write was rolled back. Do not retry a non-idempotent write without checking its effect. Back up files before repair, and close each successful open.

In 2.0, all seven file-error constructors require context.

Before:

let assert Error(slate.AccessDenied) = set.insert(table, "key", "value")
let error = slate.NeedsRepair

After:

import gleam/option.{None}
let assert Error(slate.AccessDenied(context)) = set.insert(table, "key", "value")
let error = slate.NeedsRepair(
slate.FileErrorContext(path: None, reason: "application requires repair"),
)

Use (_) instead of (context) when you only need the category. Update all seven constructors, including errors inside set.TableError. NotFound, KeyAlreadyPresent, and DecodeErrors are unchanged.

No DETS file-format migration is needed unless your application stores DetsError values as data. The error_code and error_message outputs are unchanged. This change does not alter DETS ownership or cleanup behavior.

ErrorCauseTypical functions
NotFoundKey missing (set tables only)set.lookup, set.update_counter (via TableError)
KeyAlreadyPresentKey (set) or exact entry (bag) already existsinsert_new (set, bag)
AccessDenied(_)File access denied or write on read-only tableopen*, is_dets_file, write operations
TypeMismatch(_)Wrong table type or key position for fileopen, open_with, open_with_access
FileNotFound(_)File or parent directory missingopen*, is_dets_file
NotADetsFile(_)Path exists but is not a DETS fileopen* (is_dets_file returns Ok(False))
NeedsRepair(_)File not closed cleanly, opened with NoRepairopen_with, open_with_access
AlreadyOpen(context)Table open with incompatible optionsopen*
TableDoesNotExistInvalid table handle (already closed)Most operations
FileSizeLimitExceeded(_)Write would exceed 2 GBWrite operations
TableNamePoolExhaustedToo many tables open at onceopen*
DecodeErrors(_)On-disk data did not match decodersRead operations
UnexpectedError(_)Unexpected Erlang-level errorAny