Skip to content

Troubleshooting

This page covers common issues you may encounter when working with slate, along with explanations and recovery strategies.

DecodeErrors is returned when data stored on disk does not match the Gleam decoders you provided when opening the table. This typically happens when:

  • Schema changes — you changed the value type (e.g., from String to Int) but the file still contains data written with the old schema.
  • Wrong file — you opened a DETS file that was written by a different application or with different key/value types.
  • Manual edits — the file was modified outside of slate.

Pattern match on the DecodeErrors variant to inspect the list of decode.DecodeError values. Each entry describes what the decoder expected versus what it found on disk:

import gleam/dynamic/decode
import gleam/io
import gleam/list
import slate
import slate/set
case set.lookup(table, key: "user:1") {
Ok(value) -> io.println("Found: " <> value)
Error(slate.DecodeErrors(errors)) -> {
list.each(errors, fn(err) {
io.println(
"Expected " <> err.expected <> " at " <> err.path
<> ", got: " <> err.found,
)
})
}
Error(other) -> io.println(slate.error_message(other))
}
  • Re-create the table: Delete the .dets file and let slate create a fresh one on the next open call. This is the simplest option when the old data is expendable.
  • Migrate data: Open the old table using decoders that match the old schema, read all entries with fold, then write them into a new table with the updated schema.
  • Use flexible decoders: If your schema evolves over time, design your decoders to accept multiple shapes using decode.one_of.

When a DETS file is not closed properly — for example, after a crash or power failure — it may be in an inconsistent state. The RepairPolicy controls what happens when slate opens such a file.

PolicyBehaviorUse when
AutoRepairSilently repairs corruption on openDefault for most applications. Good for caches and data that should "just work."
ForceRepairRebuilds the file even if no corruption is detectedRecovering from suspected silent corruption, or after a crash where you want to be certain the file is consistent.
NoRepairReturns an error if corruption is foundProduction systems where you want to detect corruption explicitly and handle it on your own terms.

If your application crashed and you suspect the DETS file may be corrupted, open it with ForceRepair to rebuild:

import gleam/dynamic/decode
import slate.{ForceRepair}
import slate/set
let assert Ok(table) =
set.open_with(path: "data/cache.dets", repair: ForceRepair,
key_decoder: decode.string, value_decoder: decode.string)

Use NoRepair when you want to know about corruption rather than silently fixing it:

import gleam/dynamic/decode
import gleam/io
import slate.{ForceRepair, NoRepair}
import slate/set
case set.open_with(path: "data/important.dets", repair: NoRepair,
key_decoder: decode.string, value_decoder: decode.int)
{
Ok(table) -> {
// File is clean, proceed normally
table
}
Error(_) -> {
io.println("Corruption detected — forcing repair")
let assert Ok(table) =
set.open_with(path: "data/important.dets", repair: ForceRepair,
key_decoder: decode.string, value_decoder: decode.int)
table
}
}

slate uses a fixed internal pool of 4096 DETS table name slots. Each unique file path that you open consumes one slot. When all slots are in use, open returns TableNamePoolExhausted.

  • Opening thousands of distinct file paths without closing them.
  • Dynamically generating file paths (e.g., one file per user) in a long-running application.
  1. Close unused tables — call close on tables you no longer need. This frees their pool slots for reuse.
  2. Reuse file paths — instead of creating a new file per entity, store multiple entities in one table using structured keys.
  3. Use with_table — the with_table helper automatically closes the table when the callback returns, preventing slot leaks.
import gleam/io
import gleam/dynamic/decode
import slate
import slate/set
case set.open("data/table_4097.dets",
key_decoder: decode.string, value_decoder: decode.string)
{
Ok(table) -> table
Error(slate.TableNamePoolExhausted) -> {
io.println("Too many open tables — close some before opening new ones")
panic as "table name pool exhausted"
}
Error(other) -> panic as slate.error_message(other)
}

The three table types return different result shapes from lookup:

Table typeReturn typeMissing key returns
setResult(value, DetsError)Error(NotFound)
bagResult(List(value), DetsError)Ok([])
duplicate_bagResult(List(value), DetsError)Ok([])

Set tables store one value per key. lookup returns the value directly, or NotFound if the key does not exist:

import slate
import slate/set
case set.lookup(table, key: "alice") {
Ok(age) -> age
Error(slate.NotFound) -> 0
Error(other) -> panic as slate.error_message(other)
}

Bag tables store multiple values per key. lookup always returns a list — an empty list means the key was not found:

import gleam/list
import slate/bag
let assert Ok(tags) = bag.lookup(table, key: "article:1")
case list.is_empty(tags) {
True -> ["untagged"]
False -> tags
}

This difference matters when you branch on "key exists vs. key missing." With set tables, check for Error(NotFound). With bag and duplicate bag tables, check for an empty list.

The delete_key function removes all values associated with a key. In contrast, delete_object removes only the exact key-value pair you specify. This distinction is most useful with duplicate bag tables, where a single key can have multiple values — including duplicates.

import slate/duplicate_bag
// Table contains: ("color", "red"), ("color", "red"), ("color", "blue")
// delete_object removes all exact matches of the key-value pair
let assert Ok(Nil) = duplicate_bag.delete_object(table, key: "color", value: "red")
// Now contains only: ("color", "blue")
// Both copies of ("color", "red") were removed
// To remove all values for a key regardless of value, use delete_key instead
let assert Ok(Nil) = duplicate_bag.delete_key(table, key: "color")
// Now contains nothing for "color"

For bag tables, delete_object works the same way — it removes the specific key-value pair. Since bag tables do not store duplicate pairs, one call is always sufficient.

For set tables, delete_object removes the entry only if both the key and value match what is stored. If you only want to delete by key regardless of value, use delete_key instead.

slate provides two helper functions for working with errors programmatically:

  • slate.error_code(error) returns a stable, machine-readable string like "not_found" or "decode_error". Use this for logging, metrics, and programmatic error handling.
  • slate.error_message(error) returns a human-readable description like "No value was found for the requested key." Use this for user-facing messages and debug output.
ErrorCodeMessage
NotFound"not_found"No value was found for the requested key.
FileNotFound"file_not_found"The DETS file could not be found.
AlreadyOpen"already_open"The table is already open with incompatible options.
TableDoesNotExist"table_does_not_exist"The table is not currently open.
FileSizeLimitExceeded"file_size_limit_exceeded"The DETS file exceeded the 2 GB size limit.
KeyAlreadyPresent"key_already_present"The key or key-value pair is already present.
AccessDenied"access_denied"The requested operation is not allowed with the current access mode.
TypeMismatch"type_mismatch"The file was opened with the wrong DETS table type.
TableNamePoolExhausted"table_name_pool_exhausted"Too many different DETS tables are open at once.
DecodeErrors(_)"decode_error"Data on disk did not match the expected Gleam types.
UnexpectedError(_)"unexpected_error"An unexpected DETS error occurred.
import gleam/io
import slate
import slate/set
case set.lookup(table, key: "session:abc") {
Ok(value) -> Ok(value)
Error(err) -> {
// Log the stable code for monitoring and alerting
io.println("[slate:" <> slate.error_code(err) <> "] " <> slate.error_message(err))
// Branch on specific errors using pattern matching
case err {
slate.NotFound -> Error("Session not found")
slate.DecodeErrors(_) -> Error("Corrupt session data")
_ -> Error("Storage error: " <> slate.error_message(err))
}
}
}