How to handle failing badge, group and user lookups in Roblox

Badge, group and user lookups go over the web and can fail. How to wrap them in pcall so a failed call never leaves a reward half given.

warningLuaudit check: unprotected-web-calls

What it looks like

The server calls a Roblox web API, such as a badge, group, friends or user lookup, without pcall, in the middle of code that has already changed something or still has work to do. When the call fails, the error stops the rest of the function, so a reward is marked as claimed but never given, a badge is never awarded, or a join handler never finishes.

Luau
profile.Claimed.Welcome = true
if BadgeService:UserHasBadgeAsync(player.UserId, founderBadge) then
	profile.Coins += 500
end

What an exploiter could do

Usually nobody needs to exploit this: Roblox's web services fail now and then on their own, and the game loses something each time. Players miss rewards they were promised, buyers lose what they paid for, or a whole feature stops for the rest of the server because its loop died. A player who notices can also retry at busy moments to land in the failure on purpose.

How to fix it

  • Wrap each web call in pcall and decide what happens on failure before changing anything.
  • Do the web call first, then change the profile only when it succeeded. Record a claim in the same step that gives the reward.
  • In loops, catch the error so one failed call doesn't stop the loop.

Vulnerable example

Vulnerable
local BadgeService = game:GetService("BadgeService")
local Players = game:GetService("Players")
local PlayerData = require(script.Parent.PlayerData)

local founderBadge = 2124500001
local reward = 500

Players.PlayerAdded:Connect(function(player)
	local profile = PlayerData.waitFor(player)
	if not profile or profile.Claimed.Founder then
		return
	end
	profile.Claimed.Founder = true
	if BadgeService:UserHasBadgeAsync(player.UserId, founderBadge) then
		profile.Coins += reward
	end
end)

Fixed example

Fixed
local BadgeService = game:GetService("BadgeService")
local Players = game:GetService("Players")
local PlayerData = require(script.Parent.PlayerData)

local founderBadge = 2124500001
local reward = 500

Players.PlayerAdded:Connect(function(player)
	local profile = PlayerData.waitFor(player)
	if not profile or profile.Claimed.Founder then
		return
	end
	local ok, hasBadge = pcall(BadgeService.UserHasBadgeAsync, BadgeService, player.UserId, founderBadge)
	if not ok or player.Parent ~= Players then
		return
	end
	profile.Claimed.Founder = true
	if hasBadge then
		profile.Coins += reward
	end
end)

How to spot this in your own scripts, and the cases that only look like it, are part of the Luaudit check your AI runs.