mirror of
https://github.com/AmberGraceRblx/luau-promise.git
synced 2025-04-25 08:00:03 +00:00
Improve stack traces and errors
This commit is contained in:
parent
f541fb7202
commit
65daff4777
1 changed files with 119 additions and 25 deletions
144
lib/init.lua
144
lib/init.lua
|
@ -6,6 +6,7 @@ local ERROR_YIELD_NEW = "Yielding inside Promise.new is not allowed! Use Promise
|
||||||
local ERROR_YIELD_THEN = "Yielding inside andThen/catch is not allowed! Instead, return a new Promise from andThen/catch."
|
local ERROR_YIELD_THEN = "Yielding inside andThen/catch is not allowed! Instead, return a new Promise from andThen/catch."
|
||||||
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
|
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
|
||||||
local ERROR_NON_LIST = "Please pass a list of promises to %s"
|
local ERROR_NON_LIST = "Please pass a list of promises to %s"
|
||||||
|
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
|
|
||||||
|
@ -44,8 +45,6 @@ local function ppcall(yieldError, callback, ...)
|
||||||
|
|
||||||
if ok and coroutine.status(co) ~= "dead" then
|
if ok and coroutine.status(co) ~= "dead" then
|
||||||
error(yieldError, 2)
|
error(yieldError, 2)
|
||||||
elseif not ok then
|
|
||||||
result[1] = debug.traceback(result[1], 2)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return ok, len, result
|
return ok, len, result
|
||||||
|
@ -55,14 +54,14 @@ end
|
||||||
Creates a function that invokes a callback with correct error handling and
|
Creates a function that invokes a callback with correct error handling and
|
||||||
resolution mechanisms.
|
resolution mechanisms.
|
||||||
]]
|
]]
|
||||||
local function createAdvancer(callback, resolve, reject)
|
local function createAdvancer(traceback, callback, resolve, reject)
|
||||||
return function(...)
|
return function(...)
|
||||||
local ok, resultLength, result = ppcall(ERROR_YIELD_THEN, callback, ...)
|
local ok, resultLength, result = ppcall(ERROR_YIELD_THEN, callback, ...)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
resolve(unpack(result, 1, resultLength))
|
resolve(unpack(result, 1, resultLength))
|
||||||
else
|
else
|
||||||
reject(unpack(result, 1, resultLength))
|
reject(result[1], traceback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -109,6 +108,9 @@ function Promise.new(callback, parent)
|
||||||
|
|
||||||
_status = Promise.Status.Started,
|
_status = Promise.Status.Started,
|
||||||
|
|
||||||
|
-- Will be set to the Lua error string if it occurs while executing.
|
||||||
|
_error = nil,
|
||||||
|
|
||||||
-- A table containing a list of all results, whether success or failure.
|
-- A table containing a list of all results, whether success or failure.
|
||||||
-- Only valid if _status is set to something besides Started
|
-- Only valid if _status is set to something besides Started
|
||||||
_values = nil,
|
_values = nil,
|
||||||
|
@ -163,32 +165,66 @@ function Promise.new(callback, parent)
|
||||||
return self._status == Promise.Status.Cancelled
|
return self._status == Promise.Status.Cancelled
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, _, result = ppcall(ERROR_YIELD_NEW, callback, resolve, reject, onCancel)
|
local ok, _, result = ppcall(
|
||||||
local err = result[1]
|
ERROR_YIELD_NEW,
|
||||||
|
callback,
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
onCancel
|
||||||
|
)
|
||||||
|
|
||||||
if not ok and self._status == Promise.Status.Started then
|
if not ok then
|
||||||
reject(err)
|
self._error = result[1] or "error"
|
||||||
|
reject((result[1] or "error") .. "\n" .. self._source)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Promise._newWithSelf(executor, ...)
|
||||||
|
local args
|
||||||
|
local promise = Promise.new(function(...)
|
||||||
|
args = {...}
|
||||||
|
end, ...)
|
||||||
|
|
||||||
|
executor(promise, unpack(args))
|
||||||
|
|
||||||
|
return promise
|
||||||
|
end
|
||||||
|
|
||||||
|
function Promise._new(traceback, executor, ...)
|
||||||
|
return Promise._newWithSelf(function(self, resolve, reject)
|
||||||
|
self._source = traceback
|
||||||
|
|
||||||
|
executor(resolve, function(err, traceback)
|
||||||
|
err = err or "error"
|
||||||
|
traceback = traceback or ""
|
||||||
|
self._error = err
|
||||||
|
reject(err .. "\n" .. traceback)
|
||||||
|
end)
|
||||||
|
end, ...)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Promise.new, except pcall on a new thread is automatic.
|
Promise.new, except pcall on a new thread is automatic.
|
||||||
]]
|
]]
|
||||||
function Promise.async(callback)
|
function Promise.async(callback)
|
||||||
local traceback = debug.traceback()
|
local traceback = debug.traceback()
|
||||||
return Promise.new(function(resolve, reject, onCancel)
|
local promise
|
||||||
|
promise = Promise.new(function(resolve, reject, onCancel)
|
||||||
local connection
|
local connection
|
||||||
connection = RunService.Heartbeat:Connect(function()
|
connection = RunService.Heartbeat:Connect(function()
|
||||||
connection:Disconnect()
|
connection:Disconnect()
|
||||||
local ok, err = pcall(callback, resolve, reject, onCancel)
|
local ok, err = pcall(callback, resolve, reject, onCancel)
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
|
promise._error = err or "error"
|
||||||
reject(err .. "\n" .. traceback)
|
reject(err .. "\n" .. traceback)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
return promise
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -350,22 +386,32 @@ end
|
||||||
|
|
||||||
The given callbacks are invoked depending on that result.
|
The given callbacks are invoked depending on that result.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:andThen(successHandler, failureHandler)
|
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
|
||||||
self._unhandledRejection = false
|
self._unhandledRejection = false
|
||||||
|
|
||||||
-- Create a new promise to follow this part of the chain
|
-- Create a new promise to follow this part of the chain
|
||||||
return Promise.new(function(resolve, reject)
|
return Promise._new(traceback, function(resolve, reject)
|
||||||
-- Our default callbacks just pass values onto the next promise.
|
-- Our default callbacks just pass values onto the next promise.
|
||||||
-- This lets success and failure cascade correctly!
|
-- This lets success and failure cascade correctly!
|
||||||
|
|
||||||
local successCallback = resolve
|
local successCallback = resolve
|
||||||
if successHandler then
|
if successHandler then
|
||||||
successCallback = createAdvancer(successHandler, resolve, reject)
|
successCallback = createAdvancer(
|
||||||
|
traceback,
|
||||||
|
successHandler,
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local failureCallback = reject
|
local failureCallback = reject
|
||||||
if failureHandler then
|
if failureHandler then
|
||||||
failureCallback = createAdvancer(failureHandler, resolve, reject)
|
failureCallback = createAdvancer(
|
||||||
|
traceback,
|
||||||
|
failureHandler,
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
if self._status == Promise.Status.Started then
|
if self._status == Promise.Status.Started then
|
||||||
|
@ -386,11 +432,28 @@ function Promise.prototype:andThen(successHandler, failureHandler)
|
||||||
end, self)
|
end, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Promise.prototype:andThen(successHandler, failureHandler)
|
||||||
|
assert(
|
||||||
|
successHandler == nil or type(successHandler) == "function",
|
||||||
|
ERROR_NON_FUNCTION:format("Promise:andThen")
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
failureHandler == nil or type(failureHandler) == "function",
|
||||||
|
ERROR_NON_FUNCTION:format("Promise:andThen")
|
||||||
|
)
|
||||||
|
|
||||||
|
return self:_andThen(debug.traceback(), successHandler, failureHandler)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Used to catch any errors that may have occurred in the promise.
|
Used to catch any errors that may have occurred in the promise.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:catch(failureCallback)
|
function Promise.prototype:catch(failureCallback)
|
||||||
return self:andThen(nil, failureCallback)
|
assert(
|
||||||
|
failureCallback == nil or type(failureCallback) == "function",
|
||||||
|
ERROR_NON_FUNCTION:format("Promise:catch")
|
||||||
|
)
|
||||||
|
return self:_andThen(debug.traceback(), nil, failureCallback)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -398,7 +461,8 @@ end
|
||||||
value returned from the handler.
|
value returned from the handler.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:tap(tapCallback)
|
function Promise.prototype:tap(tapCallback)
|
||||||
return self:andThen(function(...)
|
assert(type(tapCallback) == "function", ERROR_NON_FUNCTION:format("Promise:tap"))
|
||||||
|
return self:_andThen(debug.traceback(), function(...)
|
||||||
local callbackReturn = tapCallback(...)
|
local callbackReturn = tapCallback(...)
|
||||||
|
|
||||||
if Promise.is(callbackReturn) then
|
if Promise.is(callbackReturn) then
|
||||||
|
@ -416,8 +480,9 @@ end
|
||||||
Calls a callback on `andThen` with specific arguments.
|
Calls a callback on `andThen` with specific arguments.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:andThenCall(callback, ...)
|
function Promise.prototype:andThenCall(callback, ...)
|
||||||
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:andThenCall"))
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:andThen(function()
|
return self:_andThen(debug.traceback(), function()
|
||||||
return callback(unpack(values, 1, length))
|
return callback(unpack(values, 1, length))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -468,14 +533,19 @@ end
|
||||||
Used to set a handler for when the promise resolves, rejects, or is
|
Used to set a handler for when the promise resolves, rejects, or is
|
||||||
cancelled. Returns a new promise chained from this promise.
|
cancelled. Returns a new promise chained from this promise.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:finally(finallyHandler)
|
function Promise.prototype:_finally(traceback, finallyHandler)
|
||||||
self._unhandledRejection = false
|
self._unhandledRejection = false
|
||||||
|
|
||||||
-- Return a promise chained off of this promise
|
-- Return a promise chained off of this promise
|
||||||
return Promise.new(function(resolve, reject)
|
return Promise._new(traceback, function(resolve, reject)
|
||||||
local finallyCallback = resolve
|
local finallyCallback = resolve
|
||||||
if finallyHandler then
|
if finallyHandler then
|
||||||
finallyCallback = createAdvancer(finallyHandler, resolve, reject)
|
finallyCallback = createAdvancer(
|
||||||
|
traceback,
|
||||||
|
finallyHandler,
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
if self._status == Promise.Status.Started then
|
if self._status == Promise.Status.Started then
|
||||||
|
@ -488,12 +558,21 @@ function Promise.prototype:finally(finallyHandler)
|
||||||
end, self)
|
end, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Promise.prototype:finally(finallyHandler)
|
||||||
|
assert(
|
||||||
|
finallyHandler == nil or type(finallyHandler) == "function",
|
||||||
|
ERROR_NON_FUNCTION:format("Promise:finally")
|
||||||
|
)
|
||||||
|
return self:_finally(debug.traceback(), finallyHandler)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Calls a callback on `finally` with specific arguments.
|
Calls a callback on `finally` with specific arguments.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:finallyCall(callback, ...)
|
function Promise.prototype:finallyCall(callback, ...)
|
||||||
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:finallyCall"))
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:finally(function()
|
return self:_finally(debug.traceback(), function()
|
||||||
return callback(unpack(values, 1, length))
|
return callback(unpack(values, 1, length))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -590,11 +669,17 @@ function Promise.prototype:_resolve(...)
|
||||||
warn(message)
|
warn(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
local promise = (...):andThen(
|
local chainedPromise = ...
|
||||||
|
|
||||||
|
local promise = chainedPromise:andThen(
|
||||||
function(...)
|
function(...)
|
||||||
self:_resolve(...)
|
self:_resolve(...)
|
||||||
end,
|
end,
|
||||||
function(...)
|
function(...)
|
||||||
|
-- The handler errored. Replace the inner stack trace with our outer stack trace.
|
||||||
|
if chainedPromise._error then
|
||||||
|
return self:_reject((chainedPromise._error or "") .. "\n" .. self._source)
|
||||||
|
end
|
||||||
self:_reject(...)
|
self:_reject(...)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
@ -652,10 +737,15 @@ function Promise.prototype:_reject(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Build a reasonable message
|
-- Build a reasonable message
|
||||||
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
local message
|
||||||
err,
|
if self._error then
|
||||||
self._source
|
message = ("Unhandled promise rejection:\n\n%s"):format(err)
|
||||||
)
|
else
|
||||||
|
message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
||||||
|
err,
|
||||||
|
self._source
|
||||||
|
)
|
||||||
|
end
|
||||||
warn(message)
|
warn(message)
|
||||||
end)()
|
end)()
|
||||||
end
|
end
|
||||||
|
@ -676,6 +766,10 @@ function Promise.prototype:_finalize()
|
||||||
callback(self._status)
|
callback(self._status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if self._parent and self._error == nil then
|
||||||
|
self._error = self._parent._error
|
||||||
|
end
|
||||||
|
|
||||||
-- Allow family to be buried
|
-- Allow family to be buried
|
||||||
if not Promise.TEST then
|
if not Promise.TEST then
|
||||||
self._parent = nil
|
self._parent = nil
|
||||||
|
|
Loading…
Reference in a new issue