Currently if you want to safely call a function, you can use pcall, xpcall, or task.spawn
In the example below I always want b to print regardless if someFunction errors. If I use pcall or xpcall, I have to implement custom error handling. Sometimes I want to use the default error handler where it shows the error and stack trace in output so I use task.spawn. But now if someFunction yields at all then b will print before the function is completed.
This is were scall (safe call) comes in. It runs the function and uses the default error handler so I can easily see if something goes wrong with the function without having to implement custom error handling and it wont stop b from printing,
local function someFunction() print('running some function') task.wait(1) print(2 + 'a') end print('a') -- Current solution if I want default error handling, doesn't yield task.spawn(someFunction) -- Ideal solution so default error handler is used and I can see any errors in output without -- having to implement custom error handling scall(someFunction) -- I always want this to print regardless if someFunction errors print('b')
Why is task.spawn a bad solution? Errors halt code, that is by design. If you don’t want them to halt wrap them in a pcall/xpcall or run it in another thread
Wanting errors to not halt code indicates some issue with how your code is structured, if an error occurs and your code keeps running theres a high likelihood something will go wrong/more errors will pop up. Doesn’t warrant a new global imo
Unfortunately there’s no way at this current time to accomplish this outside of custom error handling. Though here’s a way to have the error message look less cluttered if that’ll help any.
local function DidCodeRunSuccessfully(foo) local success, err = pcall(foo) if (not success) and (err ~= nil) then print(success, "Line " .. err:match(":(%d+:.*)")) end return success end local function IWillError() print("Hello World!") wait(2) print("AA" + 22) end DidCodeRunSuccessfully(IWillError)
Regardless, if something like scall was introduced it would be pretty neat.
I’m more in agreement with ather_adv. This is really not needed and will only add another pcall function to the mix. (fun fact, did you know ypcall exists)
If your goal is just to have the error printed to the output. xpcall can work extremely nicely with the current tools we have.
xpcall(someFunction, print) -- if you want warnings instead xpcall(someFunction, warn)
Personally I use xpcall with warn, but I don’t think there’s much more of a need than that. Having scall and programmers normalizing having errors in code just doesn’t seem like a good idea.
It’s not about normalizing making errors. It’s about catching errors, which is really useful for libraries that allow users to implement their own code for certain things.
It seems like scall would be a convenient function, although not necessary. Like he said, it’s for avoiding having to implement custom error handling. Also I’m pretty sure that with custom error handling, you can’t include code line links.
I think this could be solved with a more general ‘join’ function in the task library.
Essentially, it would yield the current thread until the passed thread(s) become ‘dead’. You could use this to yield for another thread and resume once it finishes/errors.
local function someFunction() error("uh oh") end task.join(task.spawn(someFunction)) print("always prints") local t1, t2, t3 = task.spawn(f), task.defer(g), task.delay(1, h) task.join(t1, t2, t3) -- waits until all threads are dead
This would also allow us to do other cool structured concurrency patterns, like race/rush.
Could you not do something similar to this? Admittedly, having this native to the engine would probably look nicer but this hack seems to gap-fill what your post proposes
local function scall(func, ...) local function handler(err) task.spawn( error, err .. '\n' .. debug.traceback(nil, 2) ) end task.spawn(xpcall, func, handler, ...) end
This misses the point. Your solution is just a way more complex way of doing task.spawn(someFunction).
Also your solution does not yield and it does not provide clickable links in the stack trace which are the main reasons why I think scall should exist.
They already appear to have plans in mind to provide a better solution for this, though it seems like there’s no longer a clear timeline for when it might actually be out since it’s no longer on the current roadmap.
While I’m not against this proposal in principle, I’m having a hard time thinking of a case where it’s justified to implicitly ignore an error message but somehow still get a traceback piped to the console.
Exceptions are designed to either warn against illegal operations (dividing by a string, passing in the wrong argument type) or as a response from an API when it fails (HttpService giving you a 404). If an exception is raised, you would either want it stop execution and give you a helpful traceback to pinpoint what went wrong or have a way to catch it so you can ‘unwrap’ the response and decide what to do next.
A function that consumes exceptions without a way to unwrap them seems antithetical to that design goal. What I think you really want is a way to get a ‘soft error’ that continues control flow but also gives you a traceback, which you can already achieve with xpcall and debug.traceback. It would also be doable with a theoretical task.join function, as I’ve mentioned.
My use case is for things like tools for example. Say I have a folder of tool modules and each one works independently from each other. I want to be able to call methods like Register, Equip, and Unequip in these modules and if one of those methods errors it should not break the entire system. Right now I use task.spawn but I want these methods to be able to yield and to use the default error handler. I don’t want to have to write a custom error handler, it is not necessary for my use case.
-- ToolManager local ToolManager = {} for _, ToolSource in ToolContainer:GetChildren() do local Tool = require(ToolSource) -- I want to safely call this Register method and use the default error handler if (typeof(Tool.Register) == 'function') then task.spawn(Tool.Register, Tool) end end function ToolManager:SetEquippedTool(Tool) local LastTool = self.EquippedTool if (LastTool) then self.EquippedTool = nil -- I want to safely call this Unequip method and use the default error handler if (typeof(LastTool.Unequip) == 'function') then task.spawn(LastTool.Unequip, LastTool) end end if (Tool) then self.EquippedTool = Tool -- I want to safely call this Equip method and use the default error handler if (typeof(Tool.Equip) == 'function') then task.spawn(Tool.Equip, Tool) end end end -- Tool module local Tool = {} function Tool:Register() end function Tool:Equip() end function Tool:Unequip() end return Tool
In this case, why would you even want script execution to continue when Tool.Register errors in the first place? If it errors, it means it did something unintentional and needs to be stopped and fixed.
Because it is not critical to the game. If Roblox makes some update and stops a specific tool from equipping correctly, it should not break the entire tool system and prevent other tools from being equipped. If the AC in your car stops working it shouldn’t stop the car from turning on.
You should realistically never have to worry about that. If Roblox updates are frequently breaking your scripts, then you’re probably relying on undefined/deprecated behavior. Even on the rare occasion Roblox actually screws up, you’re not going to be the only one affected.
Besides that, error suppression is probably the last thing you would want to happen in that scenario. What happens if Tool.Register errors in a live game but you continue script execution and call Tool.Equip next? Could it corrupt more critical parts of your game, like DataStores? Can you always guarantee it can’t?
If you want your systems to be more robust, you need to handle any potential failures gracefully and on a case-by-case basis, not let your game continue in a corrupted zombie state. Stopping script execution when an uncaught error is raised should be seen as a safety net, not something you have to work around.