Errors
distillate exports nineteen error classes. Each has a name that discriminates it
from a plain Error, so you can narrow with instanceof or switch on name.
They fall into five groups by cause: bad parameters, an operation two structures cannot support, a build that could not finish, a structure with no room left, and a frame that will not decode.
| Error | Thrown by | Cause |
|---|---|---|
ParamError |
constructors, create, sizing |
A parameter is out of range |
BloomParamMismatchError |
BloomFilter.union |
Filters disagree on geometry |
BlockedBloomParamMismatchError |
BlockedBloomFilter.union |
Filters disagree on geometry |
ScalableParamMismatchError |
ScalableBloomFilter.union |
Filters disagree on settings or stages |
CountMinParamMismatchError |
CountMinSketch.union |
Sketches disagree on geometry or seed |
TopKParamMismatchError |
TopK.union |
Sketches disagree on capacity |
DDSketchParamMismatchError |
DDSketch.union |
Sketches bin values differently |
RibltParamMismatchError |
RibltDecoder.push |
Peers hash under different seeds |
BinaryFuseBuildError |
BinaryFuse8.from, BinaryFuse16.from |
The peel stalled on every seed |
CuckooFullError |
CuckooFilter.add |
No room for the key after 500 moves |
CountMinOverflowError |
CountMinSketch.add, union |
A counter would pass 2^32 - 1 |
TopKOverflowError |
TopK.add, union |
A count would pass 2^32 - 1 or the total 2^53 - 1 |
SerializationError |
fromJSON, fromProtobuf, and the base of the six below |
The envelope is malformed |
TruncatedError |
fromBytes |
The frame is short |
BadMagicError |
fromBytes |
Not a DSTL frame |
UnknownVersionError |
fromBytes, fromJSON |
A format version this build cannot read |
UnknownHashVariantError |
fromBytes |
A hash scheme this build cannot reproduce |
ReservedBitsError |
fromBytes |
A newer frame sets reserved header bits |
ChecksumError |
fromBytes |
CRC32 does not match |
The seven serialization errors are exported from all thirteen structure subpaths
(distillate/bloom, distillate/blocked, distillate/fuse, distillate/hll,
distillate/scalable, distillate/cuckoo, distillate/countmin,
distillate/topk, distillate/ddsketch, distillate/minhash,
distillate/riblt, distillate/theta and distillate/kll);
distillate/frame exports the six a frame read can throw (all but
UnknownHashVariantError). The rest are exported from the subpath of the
structure that throws them, except ParamError, which is exported from
distillate/bloom, distillate/blocked, distillate/hll,
distillate/scalable, distillate/cuckoo, distillate/countmin,
distillate/topk, distillate/ddsketch, distillate/minhash,
distillate/riblt, distillate/theta and distillate/kll.
Parameter errors
Section titled “Parameter errors”ParamError
Section titled “ParamError”API reference. Extends RangeError.
Thrown when a structure is constructed or sized with a parameter outside its valid range. Specifically:
n,m,k, orcapacityis not an integer of at least 1.epsilonis not a finite number strictly between 0 and 1.seedis outside the uint32 range, orkoutside uint16.blockedBitsPerKey(epsilon)orBlockedBloomFilter.createis asked for a rate below the blocked floor near 1e-8.RibltDecoder.pushis handed a frame that does not start at the next symbol it expects. Frames must reach it in order, each once; the decoder is left as it was, so the right frame can still follow.RibltEncoder.takeorRibltDecoder.pushmeets two keys that hash alike on its own side, on the first frame: a repeated key, or a 64-bit collision. The decode could never finish. Deduplicate the keys; for a collision, pick anotherseedon both peers.
What to do: this is a programming error, not a runtime condition. The message names the offending parameter and its value. Validate at your own boundary if the value comes from configuration or user input.
import { BloomFilter, ParamError } from "distillate/bloom";
try { BloomFilter.create(1000, 1.5); // epsilon must be in (0, 1)} catch (error) { if (error instanceof ParamError) { error.name; // "ParamError" error.message; // "epsilon must be in the open interval (0, 1), got 1.5" }}For the blocked floor specifically, the fix is a different structure rather than a different number. See sizing and tuning.
Merge errors
Section titled “Merge errors”Each structure’s union requires the two sides to be built alike. None of them
silently reshapes.
BloomParamMismatchError
Section titled “BloomParamMismatchError”Thrown when BloomFilter.union is given a filter whose m, k, or
seed differs from the receiver’s.
What to do: build both sides with the same parameters. Passing the same
n and epsilon to create is enough, since the geometry is a pure function
of those two. If you construct directly, share one BloomParams object.
import { BloomFilter, BloomParamMismatchError } from "distillate/bloom";
const a = BloomFilter.create(1000, 0.01);const b = BloomFilter.create(2000, 0.01); // different capacity, different m
try { a.union(b);} catch (error) { if (error instanceof BloomParamMismatchError) { // Rebuild b at a's capacity, or merge from the source keys instead. }}BlockedBloomParamMismatchError
Section titled “BlockedBloomParamMismatchError”Thrown when BlockedBloomFilter.union is given a filter whose numBlocks
or seed differs from the receiver’s.
What to do: the same. Size both sides identically. Note the check is on
numBlocks, not on the bitsPerKey you asked for, so two filters created
with different capacities can still round to the same block count and merge
fine.
import { BlockedBloomFilter, BlockedBloomParamMismatchError,} from "distillate/blocked";
const a = BlockedBloomFilter.create(1000, 0.01);const b = BlockedBloomFilter.create(100_000, 0.01);
try { a.union(b);} catch (error) { error instanceof BlockedBloomParamMismatchError; // true}ScalableParamMismatchError
Section titled “ScalableParamMismatchError”Thrown when ScalableBloomFilter.union is given a filter whose n,
epsilon, growth, tightening or seed differs from the receiver’s, or
whose stored stage geometry differs for a stage both filters have. The message
names the setting or stage.
What to do: build both sides with the same settings. A filter restored with
fromBytes keeps the stage geometry it was written with, so merge filters
built by the same release where you can, or rebuild one side from its keys.
import { ScalableBloomFilter, ScalableParamMismatchError,} from "distillate/scalable";
const a = ScalableBloomFilter.create(1000, 0.01);const b = ScalableBloomFilter.create(1000, 0.01, { growth: 4 });
let error: unknown;try { a.union(b);} catch (e) { error = e;}error instanceof ScalableParamMismatchError; // trueCountMinParamMismatchError
Section titled “CountMinParamMismatchError”Thrown when CountMinSketch.union is given a sketch whose width,
depth, or seed differs from the receiver’s.
What to do: build both sides with the same epsilon, delta and seed.
The geometry is a pure function of epsilon and delta, so equal arguments to
create are enough.
import { CountMinParamMismatchError, CountMinSketch,} from "distillate/countmin";
const a = CountMinSketch.create(0.01, 0.01);const b = CountMinSketch.create(0.001, 0.01); // wider rows
let error: unknown;try { a.union(b);} catch (e) { error = e;}error instanceof CountMinParamMismatchError; // trueTopKParamMismatchError
Section titled “TopKParamMismatchError”Thrown when TopK.union is given a sketch whose capacity differs from
the receiver’s. The message names both values.
What to do: build both sides with the same epsilon. capacity is a pure
function of epsilon, so equal arguments to create are enough.
import { TopK, TopKParamMismatchError } from "distillate/topk";
const a = TopK.create(0.01);const b = TopK.create(0.001); // a larger map
let error: unknown;try { a.union(b);} catch (e) { error = e;}error instanceof TopKParamMismatchError; // trueDDSketchParamMismatchError
Section titled “DDSketchParamMismatchError”Thrown when DDSketch.union is given a sketch whose mapping bins values
differently: a different mapping kind, or a gamma or indexOffset more than
1e-12 relative apart. Adding counts from bins that cover different ranges would
give quantiles with no bound at all.
What to do: build both sides with the same relativeAccuracy and mapping.
The store may differ. A sketch decoded from Datadog’s protobuf carries the
sender’s mapping, and merges with one built here at the same accuracy.
import { DDSketch, DDSketchParamMismatchError } from "distillate/ddsketch";
const a = DDSketch.create(0.01);const b = DDSketch.create(0.02); // wider bins
let error: unknown;try { a.union(b);} catch (e) { error = e;}error instanceof DDSketchParamMismatchError; // trueRibltParamMismatchError
Section titled “RibltParamMismatchError”Thrown when RibltDecoder.push is given a frame encoded under a different
seed than the decoder’s. The two peers hash every key differently, so no
prefix of that stream can ever decode. Pushing a peer’s frame is the
reconciliation form of merging with a peer’s structure, and it fails the same
way: the two sides were set up apart. The decoder is left as it was.
What to do: give both peers the same seed, from configuration or as part
of the handshake that starts a sync.
import { RibltDecoder, RibltEncoder, RibltParamMismatchError,} from "distillate/riblt";
const encoder = RibltEncoder.from(["alice", "bob"], { seed: 1 });const decoder = RibltDecoder.from(["alice"]); // seed 0
let error: unknown;try { decoder.push(encoder.take(4));} catch (e) { error = e;}error instanceof RibltParamMismatchError; // trueBuild errors
Section titled “Build errors”BinaryFuseBuildError
Section titled “BinaryFuseBuildError”Thrown when the Binary Fuse peel fails on all 100 seeds it tries. The build hashes the keys, then peels a 3-hypergraph; a peel can stall, so it retries with a bumped seed rather than returning a corrupt filter.
What to do: in practice, nothing, because you will not see this. The probability of 100 consecutive stalls is astronomically small for any real key set. If it does happen, treat it as a bug report rather than something to retry, and include the key count. The one thing it guarantees is that you never receive a filter that would produce false negatives.
import { BinaryFuse8, BinaryFuseBuildError } from "distillate/fuse";
try { const filter = BinaryFuse8.from(["alice", "bob", "carol"]); filter.has("alice"); // true} catch (error) { if (error instanceof BinaryFuseBuildError) { // Astronomically unlikely, and never a silently wrong filter. }}Capacity errors
Section titled “Capacity errors”CuckooFullError
Section titled “CuckooFullError”Thrown when CuckooFilter.add finds both of the key’s buckets full and 500
displacements, each moving a stored fingerprint to its other bucket, still find
no empty slot. Every displacement is undone before the throw, so the filter is
exactly as it was: no key it held is lost.
What to do: the filter holds more keys than it was sized for, since a filter
created for n takes n keys. Rebuild it with a larger n, or delete keys you
no longer need before adding more.
import { CuckooFilter, CuckooFullError } from "distillate/cuckoo";
const f = CuckooFilter.create(1, 0.01);let error: unknown;for (let i = 0; i < 20 && error === undefined; i++) { try { f.add(`key-${String(i)}`); } catch (e) { error = e; }}error instanceof CuckooFullError; // trueCountMinOverflowError
Section titled “CountMinOverflowError”Thrown when an add or a union would carry a counter past 2^32 - 1,
or the total past 2^53 - 1. The sketch is left exactly as it was: a counter
that wrapped would read as an underestimate, the one outcome Count-Min rules
out.
What to do: a counter that full means a key has been counted four billion times in one cell. Split the stream across sketches by time window, or scale the counts you add down before recording them.
import { CountMinOverflowError, CountMinSketch } from "distillate/countmin";
const s = new CountMinSketch({ width: 8, depth: 2 });s.add("a", 0xffffffff);
let error: unknown;try { s.add("a");} catch (e) { error = e;}error instanceof CountMinOverflowError; // trueTopKOverflowError
Section titled “TopKOverflowError”Thrown when an add or a union would carry a key’s stored count past
2^32 - 1, or the total past 2^53 - 1. The sketch is left exactly as it
was, with no entry claimed for the refused key.
What to do: as for CountMinOverflowError, split the stream by time
window or scale the counts down. A stored count that large is a single key
counted four billion times.
import { TopK, TopKOverflowError } from "distillate/topk";
const s = new TopK({ capacity: 4 });s.add("a", 0xffffffff);
let error: unknown;try { s.add("a");} catch (e) { error = e;}error instanceof TopKOverflowError; // trues.count("a"); // 4294967295Serialization errors
Section titled “Serialization errors”Six specific errors, all extending SerializationError. Catch the base class
to handle any decode failure at once, or a subclass to tell the causes apart.
import { BloomFilter, SerializationError } from "distillate/bloom";
function load(bytes: Uint8Array): BloomFilter | null { try { return BloomFilter.fromBytes(bytes); } catch (error) { if (error instanceof SerializationError) return null; // rebuild instead throw error; }}None of them are retryable. The same bytes fail the same way every time, so the recovery is always to get different bytes or to rebuild from the source keys.
SerializationError
Section titled “SerializationError”Thrown directly when a JSON envelope is malformed: not an object, missing
the "distillate" tag, missing a string data field, or data that is not
valid base64. It is also the base class of the six below.
What to do: check that you passed the object toJSON produced, and that
it survived whatever transport carried it. An envelope that has been through a
schema mapper or had its keys renamed will fail here.
TruncatedError
Section titled “TruncatedError”Thrown when a frame is shorter than its 8-byte header plus 4-byte trailer, or when the body length does not match what its declared params imply.
What to do: the bytes were cut short in transit or storage. Re-fetch the whole frame. This check runs before any allocation, so a hostile length field cannot make the reader allocate.
BadMagicError
Section titled “BadMagicError”Thrown when a frame does not start with the four-byte DSTL magic, so it
was never produced by toBytes. Frames written before format version 4 carry
the older AMQF magic and land here rather than on the version check.
What to do: check what you are actually handing it. Common causes are a
pre-v4 distillate frame (re-serialize it with the version you run), a frame
from another library (a bloom-filters dump lands here), a base64 string that
was never decoded, or a Uint8Array sliced at the wrong offset.
fromBytes respects byteOffset and byteLength, so a correct subarray view
is fine; an incorrect one is not.
UnknownVersionError
Section titled “UnknownVersionError”Thrown when a frame’s version byte, or a JSON envelope’s v field, is not
the FORMAT_VERSION this build reads. That is 5 today.
What to do: compare the frame’s version byte (offset 4, or the envelope’s
v) with FORMAT_VERSION.
- The frame is newer. It was written by a later release. Upgrade
distillateon the reading side. If you build filters in one service and read them in another, upgrade the readers first. - The frame is older. No current release reads it: each release reads exactly one format version, and a format version bump is a major release. Upgrading cannot help. Rebuild the filter from its source keys with the version you run.
Being at least as new as the producer is necessary, not sufficient. See the compatibility model.
UnknownHashVariantError
Section titled “UnknownHashVariantError”Thrown when the low nibble of a frame’s flags byte names a hash or index
mapping this build cannot reproduce. Version 5 defines exactly one variant,
0, which is murmur3_x86_128 with each structure’s current index mapping.
See serialization.
What to do: rebuild the filter from the source keys with the version you run. The stored bits are unreadable without the scheme that produced them, so there is nothing to recover from the frame itself.
ReservedBitsError
Section titled “ReservedBitsError”Thrown when a frame sets a header bit the format reserves: flags bits 4-7,
byte 7, or the u32 at offset 12. The message names which one. The CRC has
already passed, so the frame is intact; it was written by a newer format that
spends that space.
What to do: upgrade distillate on the reading side. A reader must be at
least as new as the producer, and reading the frame while ignoring those bits
would give wrong answers with no error. See
serialization.
ChecksumError
Section titled “ChecksumError”Thrown when a frame’s CRC32 trailer does not match its contents. The header and params parsed, so this is corruption in the bytes rather than a foreign format.
What to do: discard the bytes and re-fetch. Do not use the filter anyway: a corrupted payload can produce false negatives, which is the one guarantee a filter is supposed to keep. Check the storage or transport path, since a CRC mismatch means something wrote or copied the frame incorrectly.