Luau "C stack overflow" error is giving an unhelpful stack trace when resuming coroutines

When causing a C stack overflow by resuming coroutines recursively, the error message provides unhelpful stack trace information. The error message gives the stack trace of the thread being resumed, instead of giving the stack trace of the thread that actually resumed it.

Copy the following code and run it in the command bar (or in a LocalScript) in Studio:

local queuedThreads = {}

for _ = 1, 1000 do
	task.spawn(function()
		queuedThreads[#queuedThreads+1] = coroutine.running()
	
		-- This is where the error says the problem is (Line 8).
		coroutine.yield()
		
		local nextThread = table.remove(queuedThreads)

		-- This is where the problem actually is (Line 13).
		task.spawn(nextThread)
	end)
end

local initialThread = table.remove(queuedThreads)
task.spawn(initialThread)

(Note: Due to confusion in the replies, code has been edited for clarity about what it’s doing. Previously queuedThreads was just called a, which is what some of the replies are referring to.)

It gives the following output:

  C stack overflow
  Stack Begin
  Script '[omitted for brevity]', Line 8
  Stack End
  C stack overflow (x6)
  Maximum event re-entrancy depth exceeded for ScriptContext.ErrorDetailed

Which references Line 8 i.e. the line that calls coroutine.yield(). This makes the programmer think that the issue is related to the call of coroutine.yield(), but in fact that call is fine and completed successfully; the actual issue just happened to occur while the resumed thread was still sitting on that line.

It would be more helpful for the error message to provide the stack trace of the thread that did the resume i.e. Line 13’s task.spawn().

This particular example is trivial, but in more complex pieces of code where the coroutine.yield() and task.spawn() calls are separated in the source code it becomes harder to figure out what the issue is.

4 Likes

u really dont need to do task.spawn on table.remove function or u just set it as nil ( table[i] = nil ) or clear it table.clear( tableVar )

1 Like

Uh no, you do not understand what the code is doing… table.remove(a) returns the thread it removed from the table, so the thread can then be passed to task.spawn().

1 Like

u can use index = nil instead, you can look on a signal module on FreeThread function that dont use table.remove a thread

your also already put task.spawn on first loop on “for” that mean you use task.spawn in task.spawn well its not needed and its useless

2 Likes

You’re using task.spawn wrong.

Try running this:

local a = {}

for _ = 1, 1000 do
	task.spawn(function()
		a[#a+1] = coroutine.running()
	
		coroutine.yield()
		
		task.spawn(table.remove, a)
	end)
end

task.spawn(table.remove, a)
1 Like

Based on the replies of the OP, they aren’t using it wrong. They’re not trying to spawn table.remove, they’re spawning the value (a thread) that’s returned by table.remove.

4 Likes

Threads that are running by task.spawn are separated from the initial thread and error happens on the spawned thread.
So unfortunately, this is not going to be changed.

1 Like

We have some ideas about including a secondary error message like ‘thread created on Script:line’, but we are not working on that yet.

1 Like

This happens because it makes the engine try to spawn a new event but it errors because you reached the maximum task.spawn depth (this is shared with events). As a result, this prevents events from being invoked and leads to really weird behavior.