Why server errors shouldn't be sent to players

Raw errors and web replies tell exploiters how your server works. How to log the details and send players a short message instead.

warningLuaudit check: errors-sent-to-clients

What it looks like

When something fails on the server, the raw error message or the web service's reply goes back to the player: returned from a RemoteFunction, sent with FireClient, or shown in a message. It's meant to help with debugging, but every player can read it.

Luau
local ok, err = pcall(callService, player, code)
if not ok then
	return false, err
end

How to detect it

  • Find pcall and xpcall results, debug.traceback() output, and HTTP reply fields (Body, StatusMessage, Headers) that are returned from OnServerInvoke or passed to FireClient, FireAllClients, attributes or GUI text.
  • Luau error messages name the script and line that failed, such as ServerScriptService.Shop:42. Errors from data store and HTTP calls describe what went wrong, and a web service's error reply can contain anything the service chose to put in it, including internal details or other users' data.
  • Roblox's access control guide says anything that reaches a client can be read by an exploiter, and that exposed server logic makes it faster and easier to find vulnerabilities.
  • Look for "debug mode" switches that send full errors only in some cases, and check they can't be turned on by the client.

What an exploiter could do

They send bad input on purpose until something fails, then read the errors. They learn the names of server scripts and modules, data store names and keys, web addresses and how the service answers, which helps them find the next weakness. If the web service's reply ever includes tokens or other players' data, they get those too.

How to fix it

  • Log the full error on the server with warn, and send the client a short, fixed message or code, such as "failed" or "try_again".
  • Never pass a web service's reply body or headers to a client. Pick out only the fields the client needs, after checking them.
  • Use the same message whether the failure came from bad input or a server problem, unless the player needs to fix their input.

Vulnerable example

Vulnerable
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local saves = DataStoreService:GetDataStore("BuildSaves")

ReplicatedStorage.Remotes.LoadBuild.OnServerInvoke = function(player: Player, slot: unknown)
	if typeof(slot) ~= "number" or slot ~= math.floor(slot) or slot < 1 or slot > 3 then
		return false, "Invalid slot"
	end
	local ok, result = pcall(saves.GetAsync, saves, `User_{player.UserId}/Slot{slot}`)
	if not ok then
		return false, debug.traceback(tostring(result))
	end
	return true, result
end

Fixed example

Fixed
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local saves = DataStoreService:GetDataStore("BuildSaves")

ReplicatedStorage.Remotes.LoadBuild.OnServerInvoke = function(player: Player, slot: unknown)
	if typeof(slot) ~= "number" or slot ~= math.floor(slot) or slot < 1 or slot > 3 then
		return false, "invalid_slot"
	end
	local ok, result = pcall(saves.GetAsync, saves, `User_{player.UserId}/Slot{slot}`)
	if not ok then
		warn(`Build load failed for {player.UserId}: {result}`)
		return false, "try_again"
	end
	return true, result
end

Common false positives

  • Fixed messages written by the developer, such as "Not enough coins", are fine.
  • Errors shown only in Studio, behind RunService:IsStudio(), don't reach live players.
  • Roblox's developer console guide says only the game owner, or group members with edit permission, can see server output. Printing errors with warn is the fix, not a finding.