ErrorConsume: Consumes yields or just errors

What is ErrorConsume?

ErrorConsume is basically a pcallBuddy that can also ensure a function doesn’t yield. The module is documented and typed to make it more friendly. :smiley:

  • ErrorConsume.eatError - (Calls pcall) Handles any potential error within a function, yielding the current thread until the function concludes.
  • ErrorConsume.eatYield - Handles any potential error within a function, discontinuing the functions execution whenever the function yields.

The usage cases are mainly for developers that create resources for others to use and cannot trust functions as inputs, as they could error or yield when they shouldn’t. For others, you could use it to wrap functions that COULD yield but shouldn’t, like for the callback of UpdateAsync.

Practical Usage:

local ErrorConsume = require("./ErrorConsume")
local dataStore = game:GetService("DataStoreService"):GetDataStore("SampleData")
-- Very similar to game:GetService("MemoryStoreService"):GetHashMap("...")
local memoryStore = require("./Memory")
-- just a wrapper for {Data: any, UserId: {number}, Metadata: {[string]: string}}
local dataObject = require("./DataObject")

local cache = {}

function updateExpansion(key: string, data: dataObject.Object, store: dataObject.Object)
	if store == nil then return 
		data.Data, data.UserId, data.Metadata 
	end
	-- assume both data and store have a metatable with __eq set
	if data == store then return nil end
	if data == nil then
		-- this could pull from a cache or call GetAsync
		data = memoryStore:Get(key)
	end
	--...
end

local currentKey = nil
local currentData = nil
function updateEncloser(store: any, info: DataStoreKeyInfo)
	local key = currentKey
	local data = currentData
	local oldData = dataObject.create(store, info:GetUserIds(), info:GetMetadata())
	local success, data, userId, metadata = ErrorConsume.eatYield(function()
		return updateExpansion(key, data, oldData)
	end)

	if success then
		return data, userId, metadata
	else
		return nil
	end
end

local success, data, info: DataStoreKeyInfo = ErrorConsume.eatError(function()
	currentKey = "Key"
	currentData = cache[currentKey]
	return dataStore:UpdateAsync(currentKey, updateEncloser)
end)

It isn’t the most performant, and if you know a way to enforce antiyielding without having to close the thread, by all means.

Code to insert into a ModuleScript
local ErrorConsume = {}

local function consumer(f : () -> (), ...)
	task.wait()
	return f(...)
end

--[[
	Handles any potential error within a function, yielding the current thread until the function concludes.
	@ Should an error exist, the output will be (false, errorMessage)
	@ Should an error not exist, the output will (true, ...) where ... is whatever the function returns
	
	@param f the input function to be handled
	@param ... the arguments to pass into the handled function
	@returns boolean, ...any
	
	@example
	local ErrorConsume = require(...)
	local function upper(name : string) : string
		return name:upper()
	end
	ErrorConsume.eatError(upper, Instance.new("Part")) -- Result:  false, upper is not a valid member of Part "Part" 
]]
ErrorConsume.eatError = function<T...>(f : (...any)->(T...), ...) : (boolean, T...)
	return pcall(consumer, f, ...)
end

--[[
	Handles any potential error within a function, discontinuing the functions execution whenever the function yields.
	@ WARNING: A function may still take seconds or minutes without yielding.
	@ Should an error exist, the output will be (false, errorMessage)
	@ Should an error not exist, the output will (true, ...) where ... is whatever the function returns
	
	@param f the input function to be handled
	@param ... the parameters to pass into the handled function
	@returns boolean, ...any
	
	@example
	local ErrorConsume = require(...)
	local function upper(name : string) : string
		task.wait() -- purely for the example, halts here!!
		return name:upper()
	end
	ErrorConsume.eatYield(upper, Instance.new("Part")) -- Result:  true  

]]
ErrorConsume.eatYield = function<T...>(f : (...any)->(T...), ...) : (boolean, T...)
	local coro = coroutine.create(f)
	local output = {coroutine.resume(coro, ...)}
	coroutine.close(coro) -- cannot yield
	return unpack(output)
end

return ErrorConsume
1 Like

Do you have a clear comparison of “error eating” behavior because I’m not sure what you’re referring to.

print(pcall(function()
	error("HI")
end))

print(pcall(function()
	task.wait()
	error("HI2")
end))

Both snippets run without errors

1 Like

?? I run the first and am getting

Yes

You have your debug behavior set to “on unhandled exceptions”

1 Like

I think you mean “On All Exceptions”, exceptions are “handled” when they are reached inside of a pcall, “Unhandled Exceptions” would be errors outside of a pcall.

2 Likes

Oh… Didn’t even know that existed, I guess the only use would be for consuming yields then. Thanks for that knowledge both of you! Makes it very embarrassing since I spent weeks on that portion :sweat_smile:, but glad that I don’t have to waste more time trying to not use task.wait() :slight_smile: .