Skip to content

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.

API reference. Extends RangeError.

Thrown when a structure is constructed or sized with a parameter outside its valid range. Specifically:

  • n, m, k, or capacity is not an integer of at least 1.
  • epsilon is not a finite number strictly between 0 and 1.
  • seed is outside the uint32 range, or k outside uint16.
  • blockedBitsPerKey(epsilon) or BlockedBloomFilter.create is asked for a rate below the blocked floor near 1e-8.
  • RibltDecoder.push is 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.take or RibltDecoder.push meets 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 another seed on 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.

Each structure’s union requires the two sides to be built alike. None of them silently reshapes.

API reference.

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.
}
}

API reference.

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
}

API reference.

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; // true

API reference.

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; // true

API reference.

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; // true

API reference.

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; // true

API reference.

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; // true

API reference.

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.
}
}

API reference.

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; // true

API reference.

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; // true

API reference.

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; // true
s.count("a"); // 4294967295

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.

API reference.

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.

API reference.

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.

API reference.

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.

API reference.

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 distillate on 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.

API reference.

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.

API reference.

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.

API reference.

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.