Error reporting for Roblox games. Captures unhandled errors on the server and the client and posts them to a Discord webhook as formatted embeds.
There is no backend service to run. The SDK ships as a single Luau module. Client errors are forwarded to the server through an internal RemoteEvent because Roblox only allows outbound HTTP from the server.
Unhandled errors via ScriptContext.Error on both sides, manual captureException and captureMessage, a wrap helper for risky callbacks, per-minute rate limiting, and fingerprint based dedupe so the same error firing every frame does not flood your channel.
Each Discord embed includes the message, stack trace, the script path that raised it, PlaceId, PlaceVersion, JobId, the side (server or client), the environment, the release string, your tags, and the player when the error came from the client.
This is a Rojo project. Build it once:
rojo build -o Watcher.rbxmDrop Watcher.rbxm into ReplicatedStorage, or use rojo serve to sync live.
The project file mounts the module at ReplicatedStorage.Watcher.
HttpService.HttpEnabled must be on (Game Settings, Security).
You need to call Watcher.init on both sides. The server call needs the webhook URL. The client call sets up the RemoteEvent bridge.
Server script in ServerScriptService:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Watcher = require(ReplicatedStorage.Watcher)
Watcher.init({
webhookUrl = "https://discord.com/api/webhooks/...",
environment = "production",
release = "v0.1.0",
tags = { service = "main-game" },
})LocalScript in StarterPlayer.StarterPlayerScripts:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Watcher = require(ReplicatedStorage.Watcher)
Watcher.init({
environment = "production",
release = "v0.1.0",
})The webhookUrl is ignored on the client. Do not put your webhook URL in client code.
local ok, err = pcall(doThing)
if not ok then
Watcher.captureException(err, { tags = { area = "shop" } })
endErrors raised inside the wrapped function are reported and then re-raised, so existing error handling keeps working.
Players.PlayerAdded:Connect(Watcher.wrap(function(player)
-- ...
end))Watcher.captureException(err, Watcher.addPlayerContext(player))| Field | Default | Notes |
|---|---|---|
webhookUrl |
required on server | Discord webhook URL. Ignored on client. |
environment |
"unknown" |
e.g. production, staging |
release |
"unknown" |
game version string |
tags |
{} |
merged into every event |
rateLimitPerMinute |
30 |
hard cap before events are dropped |
dedupeWindowSeconds |
60 |
suppress same fingerprint within window |
captureUnhandled |
true |
hook ScriptContext.Error |
username |
"Watcher" |
webhook display name |
A fingerprint is built from the first line of the error and the originating script. Line numbers and memory addresses are normalized so a flickering error groups as one.
examples/basic_usage.server.lua shows a typical server setup. examples/error_tests.server.lua and examples/error_tests.client.lua fire a handful of different error shapes on a timer, useful for verifying that the webhook actually receives events.
MIT