Skip to content

ErrorWatch

ErrorWatch captures every script error, deduplicates identical ones, and reports each unique error once per window with an occurrence count — a crash loop becomes one line with (x4000), not four thousand lines. It’s a shared module: it attaches to a server kernel or a client kernel the same way. For the sibling module that derives GC health instead of error telemetry, see GcWatch.

ErrorWatch merges two error streams into one deduplicated report:

  1. ScriptContext.Error — every unhandled script error on this machine, whether or not the code involves the kernel.
  2. Scheduler.OnError — task crashes the Scheduler caught and isolated. These never reach ScriptContext (the scheduler pcalls its tasks), so without this second tap a crash-looping scheduled task would be invisible to error telemetry. The error’s first line becomes the message, the full text the trace, and the source reads KernelScheduler.

Both streams funnel through the same dedup:

  • Key = the error message plus the first line of the traceback. Two different call sites raising the same message count separately; the same line crashing 4000 times counts once.
  • Window (default 30s): the first report of a key logs immediately — through Logger scope ErrorWatch at error level, formatted {message} [{source}] (x{count}) — and publishes Kernel.ScriptError on the bus. Repeats within the window increment the count silently. The first report after the window logs and publishes again, carrying the accumulated count.
  • Eviction: interpolated values (names, ids, coordinates) mint a unique key each, and without eviction the Seen table would grow for the server’s whole lifetime. At most once per window, entries unseen for WindowSeconds * 10 (default 300s) are pruned.

The result: sinks and the bus see at most one line per unique error per window, while nothing is ever dropped from the count.

-- src/Server/Bootstrap.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ErrorWatch = require(ReplicatedStorage.ChloeKernel.ErrorWatch)
return function(kernel)
local Watcher = ErrorWatch.attach(kernel)
-- Ship every deduplicated occurrence to your telemetry
kernel.Bus:subscribe("Kernel.ScriptError", function(_, message, trace, source, count)
warn(`[ops] {source}: {message} (x{count})`)
end)
end

For webhook alerting on errors generally (not just script errors), add a Logger sink filtered to Logger.Level.Error — ErrorWatch’s windowing means the sink receives one line per unique error per window, so the webhook cannot be flooded by a crash loop.

Member Description
ErrorWatch.attach(kernel?, options?) → watcher Connects ScriptContext.Error, and kernel.Scheduler.OnError when a kernel is given. kernel is optional: without one, errors still log through the Logger, but nothing publishes on the bus.
watcher:counts() → { [key]: count } Snapshot of occurrence counts, keyed by the dedup key (message .. "\n" .. firstTraceLine).
watcher:destroy() Disconnects both captures and clears the seen table.

Options (all optional):

Field Default Description
WindowSeconds 30 Dedup window. One log line and one bus publish per unique error per window. Eviction horizon is WindowSeconds * 10.
SkipBind false Do not connect ScriptContext.Error (specs drive reports directly).
Topic Payload Fired
Kernel.ScriptError (message, trace, sourceName, count) On the first occurrence of a unique error, then at most once per window with the accumulated count. sourceName is the erroring script’s GetFullName(), "?" when unknown, or "KernelScheduler" for scheduler-caught task crashes.

ErrorWatch consumes Scheduler.OnError (a signal on the scheduler, not a bus topic) when attached with a kernel.

  • attach requires the Logger from its own kernel clone (a relative require) — a cloned kernel gets its own ErrorWatch logging rather than silently sharing the original’s sinks.
  • ErrorWatch is fully spec-verified with a SkipBind double driving reports directly — the dedup windows and bus payloads above are asserted behavior, not documentation optimism.