Skip to content

ErrorWatch

Captures script errors and scheduler task crashes, and reports each unique error once per window with an occurrence count.

Realm
Shared
Require
ReplicatedStorage.ChloeKernel.ErrorWatch
Since
v0.1.0
Source
ErrorWatch.luau

ErrorWatch listens to every script error on its machine, groups identical ones, and reports each unique error once per window with a running count. A task that crashes 4,000 times in 30 seconds becomes one log line ending in (x4000) instead of 4,000 lines. It attaches the same way on a server or a client kernel, and nothing in the framework attaches it for you. GcWatch is the sibling for memory health; reports go out through Logger and the Bus.

ErrorWatch merges two streams into one report:

  1. ScriptContext.Error: every unhandled script error on this machine, kernel code or not.
  2. kernel.Scheduler.OnError: task crashes the Scheduler caught. The scheduler runs tasks protected, so these never reach ScriptContext, and a crash-looping scheduled task would be invisible without this second connection. The error’s first line becomes the message, the full text becomes the trace, and the source reads KernelScheduler.

Both streams go through the same grouping:

  • The key is the message plus the first line of the traceback. The same message raised from two call sites counts as two errors; the same line failing 4,000 times counts as one.
  • The first report of a key logs at once through Logger.scope("ErrorWatch") at error level, formatted {message} [{source}] (x{count}), and publishes Kernel.ScriptError when a kernel was given. Repeats inside WindowSeconds (default 30) only increment the count. The first repeat after the window logs and publishes again with the accumulated count.
  • Messages that interpolate names, ids or positions create a new key each. To keep the table from growing for the whole server lifetime, a report prunes entries unseen for WindowSeconds * 10 (300 s by default), at most once per window. Pruning runs inside a report, so a quiet server keeps its table until the next error arrives.

Sinks and Bus subscribers see at most one line per unique error per window, and the count keeps every occurrence.

src/Server/Game/Services/ErrorService.luau
--!strict
-- ErrorService: groups script errors and flags crash loops.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ErrorWatch = require(ReplicatedStorage.ChloeKernel.ErrorWatch)
local CrashLoopCount = 1000
local ErrorService = {
Name = "ErrorService",
}
function ErrorService.init(self: any, kernel: any)
self.Kernel = kernel
end
function ErrorService.start(self: any)
local Kernel = self.Kernel
self.Watcher = ErrorWatch.attach(Kernel)
-- Runs once per unique error per window; count is every occurrence so far
self.Connection = Kernel.Bus:subscribe("Kernel.ScriptError", function(_topic, message, _trace, source, count)
if count >= CrashLoopCount then
Kernel.Bus:publish("Ops.CrashLoop", source, message, count)
end
end)
end
function ErrorService.stop(self: any)
if self.Connection then
self.Connection:disconnect()
end
if self.Watcher then
self.Watcher:destroy()
end
end
return ErrorService

Analytics with WatchErrors = true turns each report into a ScriptError event for a custom Destination. To alert on every error-level log line, add a Logger sink that filters Logger.Level.Error. ErrorWatch’s reports reach that sink once per unique error per window, so a crash loop cannot flood a webhook.

Member Description
ErrorWatch.attach(kernel?, options?) -> ErrorWatch Connects ScriptContext.Error (unless SkipBind) and, when the kernel has one, kernel.Scheduler.OnError. Without a kernel, reports still log but nothing publishes.
watcher:counts() -> { [string]: number } A copy of the occurrence counts, keyed by message .. "\n" .. firstTraceLine.
watcher:destroy() Disconnects both streams and clears the table.
watcher:_report(message, trace, sourceName, now?) -> boolean Feeds one error through the grouping. Returns true when it logged. The spec seam for SkipBind.

Options, all optional:

Field Default Description
WindowSeconds 30 Reporting window per key. Entries unseen for ten windows are pruned.
SkipBind false Do not connect ScriptContext.Error. Specs feed _report directly.
Topic Payload Fired
Kernel.ScriptError (message, trace, sourceName, count) On the first occurrence of a key, then at most once per window with the accumulated count. sourceName is the failing script’s GetFullName(), "?" when unknown, or "KernelScheduler" for a scheduler task crash. Only when attached with a kernel.

ErrorWatch also consumes Scheduler.OnError, a Signal on the scheduler rather than a Bus topic.

  • ErrorWatch requires Logger relatively, so a cloned kernel logs through its own Logger and sinks instead of the original’s.
  • ErrorWatch.spec.luau drives _report with SkipBind and asserts the first log and publish, silent counting inside the window, the accumulated count after it, and that different errors never suppress each other.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.