Roblox Studio error

attempt to yield across metamethod/C-call boundary

What attempt to yield across metamethod/C-call boundary means in Roblox Studio, the three things that cause it with the code that triggers each, and the fix for each one.

Updated 4 November 20264 min readErrorLuauWritten by

In short

Something that waits ran inside a function that Luau calls for you and cannot pause: the comparator you gave table.sort, the function you gave string.gsub, or a metamethod such as __index. Waiting means task.wait, WaitForChild, or any call ending in Async (data stores, GetNameFromUserIdAsync, HTTP). Do the waiting first, store the results in a table, then sort, replace or look them up with no waiting inside.

On this page
  1. What it means
  2. Causes and fixes
  3. Fix it with RoCode
  4. Find the line
  5. Questions
  6. Sources

What the message means

Output (example)
attempt to yield across metamethod/C-call boundary

A Roblox script can pause (yield) and continue later. That works only when every function between the pause and the script's start can be paused too. Some functions are run by Luau itself, in C: table.sort calling your comparator, string.gsub calling your replacement function, and the engine calling a metamethod. Those cannot be paused halfway, so a yield inside them stops with this error.

pcall is not one of them in current Luau: you can wait inside a pcall. The message itself may not carry a line number: read the stack lines under it in Output, which name the script and the line of the sort, gsub or lookup that started it.

Causes and fixes

Three things cause this error in practice. Each one below has the smallest code that triggers it, then the fix.

The table.sort comparator waits

Sorting a leaderboard by player name, and fetching each name inside the comparator, is the classic case. GetNameFromUserIdAsync waits for a web request, and table.sort cannot wait.

Breaks Script in ServerScriptService named Leaderboard
local Players = game:GetService("Players")

local userIds = { 156, 1, 261 }

table.sort(userIds, function(a, b)
	-- yields inside the comparator: attempt to yield across metamethod/C-call boundary
	return Players:GetNameFromUserIdAsync(a) < Players:GetNameFromUserIdAsync(b)
end)

Fetch everything the comparator needs before you sort, and keep the comparator to plain comparisons. As a bonus each name is fetched once instead of on every comparison.

Fixed Script in ServerScriptService named Leaderboard
local Players = game:GetService("Players")

local userIds = { 156, 1, 261 }

local names: { [number]: string } = {}
for _, userId in userIds do
	local ok, name = pcall(Players.GetNameFromUserIdAsync, Players, userId)
	names[userId] = if ok then name else tostring(userId)
end

table.sort(userIds, function(a, b)
	return names[a] < names[b]
end)
print(userIds)

An __index metamethod waits

A "lazy" table that finds objects the first time you ask for them often calls WaitForChild in its __index. The first lookup of a missing key yields inside the metamethod.

Breaks Script in ServerScriptService named Remotes
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Remotes = setmetatable({}, {
	__index = function(self, name)
		-- WaitForChild yields: attempt to yield across metamethod/C-call boundary
		local remote = ReplicatedStorage:WaitForChild(name)
		rawset(self, name, remote)
		return remote
	end,
})

local buy = Remotes.BuyItem
print(buy)

Use a normal function for anything that may wait, and keep metamethods instant. Code that calls Remotes.get("BuyItem") reads just as clearly and can wait safely.

Fixed Script in ServerScriptService named Remotes
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local cache: { [string]: Instance } = {}

local Remotes = {}

function Remotes.get(name: string): Instance
	local found = cache[name]
	if not found then
		found = ReplicatedStorage:WaitForChild(name)
		cache[name] = found
	end
	return found
end

local buy = Remotes.get("BuyItem")
print(buy)

A string.gsub callback waits

Filling placeholders in a message with values that take a request to get, such as {name} replaced by a user name, puts the yield inside the function gsub calls.

Breaks Script in ServerScriptService named Announce
local Players = game:GetService("Players")

local template = "Top player: {156}"

local text = string.gsub(template, "{(%d+)}", function(id)
	-- yields inside gsub: attempt to yield across metamethod/C-call boundary
	return Players:GetNameFromUserIdAsync(tonumber(id) or 0)
end)
print(text)

Collect the placeholders first with string.gmatch, fetch their values in a normal loop, then pass gsub a table of ready answers. gsub accepts a table as the replacement and looks each capture up in it.

Fixed Script in ServerScriptService named Announce
local Players = game:GetService("Players")

local template = "Top player: {156}"

local values: { [string]: string } = {}
for id in string.gmatch(template, "{(%d+)}") do
	local ok, name = pcall(Players.GetNameFromUserIdAsync, Players, tonumber(id) or 0)
	values[id] = if ok then name else "someone"
end

local text = string.gsub(template, "{(%d+)}", values)
print(text)

Fix it in your place

Paste the error, RoCode fixes it in your place

Copy the line from Output and paste it here. RoCode opens the script it names in the place you have open in Studio, works out which of the causes above it is, and edits the code there instead of handing you a snippet to paste.

For example: attempt to yield across metamethod/C-call boundary

RoCode is an AI agent for Roblox Studio: it reads the scripts in the place you have open, finds the line and edits it there, and every change is an undo point. It runs on Windows or macOS with a desktop browser. Free to start, with free credits every day.

How to find the line that causes it

  1. Open Output (Window menu, or the Script tab), clear it with Ctrl+K (⌘K on Mac), and press Play.
  2. Find the first red line. It starts with the script's place in the Explorer and the line number, such as ServerScriptService.Shop:12:.
  3. Click the red line. Studio opens that script with the cursor on the failing line.
  4. Read the lines between Stack Begin and Stack End under it: they list the calls that led there, newest first, so you can see which function called the broken one.
  5. Still not sure which value is wrong? Add print(typeof(x), x) just above the line, or set a breakpoint on it and read the values in the Watch window.

Questions

Which functions count as yielding in Roblox?

task.wait, WaitForChild on an object that is not there yet, Event:Wait(), InvokeServer and InvokeClient, and the methods whose names end in Async, such as data store calls and GetNameFromUserIdAsync.

Can I use task.wait inside pcall?

Yes. Current Luau lets a function wait inside pcall. The places that cannot wait are callbacks Luau runs itself, such as the table.sort comparator, the string.gsub replacement function and metamethods like __index.

Could task.spawn inside the metamethod fix it?

It stops the error, because the waiting moves to a new thread, but the metamethod then returns before the value exists. Move the wait out of the metamethod instead, so the caller gets a real value.

Can RoCode fix it?

Yes. Paste the Output line into RoCode. It reads the script in the place you have open, finds the wait inside the sort, gsub or metamethod, and moves it out so the results are fetched first.

Sources and further reading

How the code was checked: every block marked Fixed passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 4 November 2026), and every block marked Breaks compiles and fails only when it runs. The message wording was checked against the message strings in Roblox Studio itself (macOS build installed on 24 October 2026) on 4 November 2026, and each cause was reproduced with the Luau command line tool the same day: a yield inside a table.sort comparator, a string.gsub callback and an __index metamethod each print this message. A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

More Roblox errors

All Roblox Studio errors

Stop hunting errors line by line.

RoCode reads the scripts in your open place, fixes the error where it happens and tells you what it changed. Free every day, no card required.