Is my Promise implementation correct?

I made small and simple implementation of promises. It wraps all the functions in a coroutine and then makes the returned value available to the next next/catch/await (“then” is named “next” because I can’t use a keyword as a key but I can use a global).

They aren’t allowed to be directly accessed through the object because of my __index function. It also wraps the functions in another function so I can access them using a . instead of a : (which looks better while chaining imo)

Is how my implementation functions correct/can I make it more efficient in any way?

local Promise = {}
Promise.__index = function(self, key)
	if key == "new" then return Promise[key] end
	
	return function(handler)
		return Promise[key](self, handler)
	end
end

function Promise.new(func)
	local self = setmetatable({}, Promise)
	
	coroutine.wrap(func)(
		function(value) -- resolve
			self._returned = value
		end,
		
		function(value) -- reject
			self._error = value
		end
	)
	
	return self
end

function Promise:await()
	return self._returned or self._error
end

function Promise:next(func)
	local success, result = pcall(function()
		return coroutine.wrap(func)(self._returned)
	end)
	
	return self.new(function(resolve, reject)
		(success and resolve or reject)(result)
	end)
end

function Promise:catch(func)
	return coroutine.wrap(func)(self._error)
end

return Promise
--// thanks for stopping by

Here is how I tested it:

--// test 1
local Promise = require(path.to.promises)

Promise.new(function(resolve, reject)
    resolve("y")
end)

.next(function(value)
    return value .. "es"
end)

.next(function(value)
    print(value) --> yes
    return value .. 3
end)

.catch(function(err)
    print(err) --> attempt to concat number and string
end)
--// test 2
local Promise = require(path.to.promises)

local result = Promise.new(function(resolve, reject)
    resolve(4)
end).await()

print(result) --> 4
1 Like

I can’t quite write a full review as I’m on the move at the moment, but I can address a few concerns:

Yes, using : may not be as beautiful as you might like, but the Luau VM optimizes method calls and this codebase is not idiomatic of Lua or Luau. To put it simply, you’re going through loops to confuse any end-user at a later date, stripping away performance, and making a less-maintainable option which, well, I definitely don’t think is a plus.

Moving right along, I’m seeing a lot of metaprogramming. Functional uses of metatables for things besides prototype-based object-oriented programming. Also, your namespaces for the static side of Promise and the instance side are a bit muddled through new. These changes are pretty needless and seem to be traps for the future.

So, yes, this minimal implementation works but a quick refactor may not hurt it a bit.

1 Like

What did you mean by this part? How should I change the .new()?

Just declare the Promise.new function and don’t use self.new to refer to Promise.new. The naming resolution you had before, where a static and instance function have the same resolution achieved through a metatable, is not commonplace for any programming language I’ve seen before.

1 Like

Can you show what you meant by the first and fourth points? I don’t know how to implement it.

Here’s my updated version:

local Promise = {}
Promise.__index = Promise

local function wrapPcall(...)
	local result = {pcall(...)}
	
	return result[1], table.remove(result, 1)
end

function Promise.new(func, chained)
	local self = setmetatable({
        _Chained = chained or {}
    }, Promise)
	
	coroutine.wrap(func)(
		function(...) -- resolve
			self._Returned = {...}
		end,
		
		function(...) -- reject
			self._Returned = {...}
		end
	)
	
	table.insert(self._Chained, self)
	return self
end

function Promise:await()
	return table.unpack(self._Returned)
end

function Promise:next(func)
	local success, results = wrapPcall(coroutine.wrap(func), self._Returned)
	
	return Promise.new(function(resolve, reject)
		(success and resolve or reject)(table.unpack(results))
	end, self._Chained)
end

function Promise:catch(func)
	local success, results = wrapPcall(coroutine.wrap(func), self._Returned)
	
	return Promise.new(function(resolve, reject)
		(success and resolve or reject)(table.unpack(results))
	end, self._Chained)
end

function Promise:finally(func)
	return coroutine.wrap(func)(self:await())
end

return Promise
--// thanks for stopping by