Skip to content

Diagnostics

Gaffer surfaces diagnostics while validating and running projections: they appear on the gaffer dev summary, in your editor’s Problems panel, and on FeedResult.diagnostics in the testing library. Every diagnostic has a stable <class>.<subject>.<detail> code.

  • quirk.* reproduces a KurrentDB engine bug so local runs match production. A quirk fires whenever quirks_version is unset (the default, which reproduces every known quirk), or is set to a release earlier than its Fixed in version. Targeting that release or later turns it off.
  • usage.* flags something about your own projection code: an unsupported construct, a no-op under an engine version, or a deprecation.

Severity is Error (no correct form: it throws or is unsupported), Warning (runs but produces a wrong or surprising result), or Information (works, but noteworthy).

Reproduced KurrentDB engine bugs, gated by quirks_version.

Severity: Error · Fixed in: not yet shipped upstream

linkStreamTo(stream, link, metadata) with a third (metadata) argument crashes in the KurrentDB projection engine - the metadata branch reads an out-of-bounds parameter and throws. The two-argument form works; metadata is never captured. gaffer reproduces the crash.

Problem

fromAll().when({
Archived(state, event) {
linkStreamTo("archive-" + event.streamId, event.streamId, { reason: "x" }); // 3-arg form crashes
return state;
}
});

Fix

fromAll().when({
Archived(state, event) {
linkStreamTo("archive-" + event.streamId, event.streamId); // two args; metadata isn't captured
return state;
}
});

Severity: Warning · Fixed in: not yet shipped upstream

log() with more than one argument behaves oddly in the KurrentDB engine: primitive arguments are emitted as separate log lines, and objects are joined with a , separator. Pass a single pre-formatted argument to avoid surprises.

Problem

fromAll().when({
Ping(state, event) {
log("seen", event.streamId); // multiple args render oddly
return state;
}
});

Fix

fromAll().when({
Ping(state, event) {
log(`seen ${event.streamId}`);
return state;
}
});

Severity: Error · Fixed in: KurrentDB 26.2.0

Accessing event.body throws in the KurrentDB engine when the event body is a non-object JSON value - null, or a primitive like a number or string. The upstream EnsureBody casts to an object without a type check. Use event.bodyRaw and parse it yourself. (gaffer’s JS testing library normalizes a data: null event to an absent body, so a null body won’t reproduce the throw there.)

Problem

fromAll().when({
Measured(state, event) {
return { latest: event.body }; // throws when the body is a primitive (42, "x", null)
}
});

Fix

fromAll().when({
Measured(state, event) {
return { latest: JSON.parse(event.bodyRaw) }; // parse bodyRaw yourself
}
});

Severity: Error · Fixed in: KurrentDB 26.2.0

The KurrentDB engine throws when projection state contains NaN or Infinity (JSON has no representation for them). Guard non-finite numbers in your handler, e.g. store null or 0.

Problem

fromAll().when({
Sampled(state, event) {
state.avg = state.total / state.count; // count 0 -> Infinity, throws on persist
return state;
}
});

Fix

fromAll().when({
Sampled(state, event) {
state.avg = state.count > 0 ? state.total / state.count : 0;
return state;
}
});

Severity: Error · Fixed in: KurrentDB 26.2.0

When a projection’s state is a bare string that isn’t valid JSON - whether a handler returned it or V1 adopted an unhandled event’s body as state - the KurrentDB engine persists it un-encoded (e.g. hello, not "hello"). On the next reload (restart, re-enable, resume) Load() runs JSON.parse on the stored value and throws, so the projection won’t resume. Wrap string state in an object (e.g. { value: "hello" }), or use KurrentDB 26.2.0+ where the engine JSON-encodes string state. (Bi-state state-array slots are unaffected - they always JSON-encode.)

Problem

fromAll().when({
Set(state, event) {
return event.body.name; // bare string state
}
});

Fix

fromAll().when({
Set(state, event) {
return { name: event.body.name };
}
});

Severity: Error · Fixed in: not yet shipped upstream

Bi-state projections (those declaring $initShared, operating on a [partitionState, sharedState] pair) are not supported under engine_version 2. The shared-state slot is not restored on restart: after a node restart, projection re-enable, or resume, the engine re-runs $initShared instead of reading the persisted shared state, silently producing incorrect results. Use engine_version 1 until KurrentDB implements shared-state restore on V2.

Problem

options({ biState: true });
fromAll().foreachStream().when({
$initShared() { return { total: 0 }; },
Deposit([state, shared], event) {
shared.total += event.body.amount;
return [state, shared];
}
});

Fix

// Run on engine_version 1; V2 does not restore shared state on restart.

Severity: Error · Fixed in: not yet shipped upstream

Bi-state projections operate on a [partitionState, sharedState] pair. A handler must mutate that pair in place or return it. If it returns anything else (a single slot, a fresh object, a scalar, or null), the KurrentDB engine persists the malformed value, then faults on the next event for that partition while restoring the pair, wedging the partition until its state is reset; gaffer reproduces that wedge. Return the [state, sharedState] pair, or omit the return and mutate it in place.

Problem

options({ biState: true });
fromAll().foreachStream().when({
$init() { return { balance: 0 }; },
$initShared() { return { total: 0 }; },
Deposited([state, shared], event) {
shared.total += event.body.amount;
return { balance: state.balance + event.body.amount }; // returns an object, not the [state, shared] pair
}
});

Fix

options({ biState: true });
fromAll().foreachStream().when({
$init() { return { balance: 0 }; },
$initShared() { return { total: 0 }; },
Deposited([state, shared], event) {
state.balance += event.body.amount;
shared.total += event.body.amount;
return [state, shared]; // return the pair (or omit the return and mutate in place)
}
});

Severity: Warning · Fixed in: not yet shipped upstream

outputState() has no effect under engine_version 2. V2 does not emit Result events to a result stream - state is written only to the $projections-{name}[-{partition}]-state stream and must be polled (or that stream subscribed to). Live result-stream parity is planned for a future release; use engine_version 1 until then if you rely on result-stream subscriptions.

Problem

fromAll().when({
Counted(state, event) { return { count: state.count + 1 }; }
}).outputState(); // no effect under engine_version 2

Fix

// Run on engine_version 1, or read the $projections-{name}-state stream.

Issues in your own projection code.

Severity: Information

linkStreamTo is undocumented in KurrentDB and may be removed in a future version. Prefer linkTo.

Problem

fromAll().when({
Archived(state, event) {
linkStreamTo("archive-" + event.streamId, event.streamId); // undocumented, may be removed
return state;
}
});

Fix

fromAll().when({
Archived(state, event) {
linkTo("archive-" + event.streamId, event); // prefer linkTo
return state;
}
});

Severity: Warning

transformBy()/filterBy() are registered but never invoked under engine_version 2 - the result equals the post-handler state. Use engine_version 1 for V1 transform behaviour.

Problem

fromAll().when({
Counted(state, event) { return { count: state.count + 1 }; }
}).transformBy(state => ({ total: state.count })); // not invoked under engine_version 2

Fix

// Produce the final shape in the handler, or run on engine_version 1.
fromAll().when({
Counted(state, event) { return { total: state.total + 1 }; }
});

Severity: Information

options() is called more than once; only the last call takes effect and earlier ones are discarded. Merge them into a single call.

Problem

options({ biState: true });
options({ resultStreamName: "results" }); // overwrites the first; biState is lost
fromAll().when({ /* ... */ });

Fix

options({ biState: true, resultStreamName: "results" });
fromAll().when({ /* ... */ });

Severity: Warning

reorderEvents/processingLag have no effect under engine_version 2 - events are processed in arrival order. Use engine_version 1 if you need event reordering.

Problem

options({ reorderEvents: true, processingLag: 100 }); // no effect under engine_version 2
fromStreams("a", "b").when({ /* ... */ });

Fix

// Run on engine_version 1 if you need event reordering.

Severity: Error

async handlers are not supported: the projection engine runs synchronously, so the returned Promise is serialized as the state (which becomes {}) instead of being awaited. Make the handler synchronous.

Problem

fromAll().when({
async Loaded(state, event) { // async isn't supported; state becomes {}
return state;
}
});

Fix

fromAll().when({
Loaded(state, event) {
return state;
}
});

Severity: Error

Returning a Promise from a handler is not supported: the engine runs synchronously, so the Promise is serialized as the state (which becomes {}) instead of being awaited. Return the state synchronously.

Problem

fromAll().when({
Loaded(state, event) {
return Promise.resolve(state); // Promise serialized as state -> {}
}
});

Fix

fromAll().when({
Loaded(state, event) {
return state;
}
});