Expose & refactor RuntimeError

This commit is contained in:
Eryn Lynn 2020-05-06 18:02:10 -04:00
parent 292e47293c
commit ff345ea31b
2 changed files with 75 additions and 25 deletions

View file

@ -17,29 +17,55 @@ local RunService = game:GetService("RunService")
Promises that experience an error like this will be rejected with Promises that experience an error like this will be rejected with
an instance of this object. an instance of this object.
]] ]]
local PromiseRuntimeError = {} local RuntimeError = {}
PromiseRuntimeError.__index = PromiseRuntimeError RuntimeError.__index = RuntimeError
function PromiseRuntimeError.new(errorString) function RuntimeError.new(options, parent)
options = options or {}
return setmetatable({ return setmetatable({
errorString = tostring(errorString) or "[This error has no error text.]" error = tostring(options.error) or "[This error has no error text.]",
}, PromiseRuntimeError) trace = options.trace,
context = options.context,
parent = parent,
createdTick = tick(),
createdTrace = debug.traceback()
}, RuntimeError)
end end
function PromiseRuntimeError.is(anything) function RuntimeError.is(anything)
if type(anything) == "table" then if type(anything) == "table" then
return rawget(anything, "errorString") ~= nil local metatable = getmetatable(anything)
if type(metatable) == "table" then
return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
end
end end
return false return false
end end
function PromiseRuntimeError:extend(errorString) function RuntimeError:extend(options)
return PromiseRuntimeError.new(("%s\n%s"):format(tostring(errorString), self.errorString)) return RuntimeError.new(options, self)
end end
function PromiseRuntimeError:__tostring() function RuntimeError:getErrorChain()
return self.errorString local runtimeErrors = { self }
while runtimeErrors[#runtimeErrors].parent do
table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
end
return runtimeErrors
end
function RuntimeError:__tostring()
local errorStrings = {}
for _, runtimeError in ipairs(self:getErrorChain()) do
table.insert(errorStrings, table.concat({runtimeError.trace or runtimeError.error, runtimeError.context}, "\n"))
end
return table.concat(errorStrings, "\n")
end end
--[[ --[[
@ -61,7 +87,11 @@ end
local function makeErrorHandler(traceback) local function makeErrorHandler(traceback)
return function(err) return function(err)
return debug.traceback(err, 2) .. "\nPromise created at:\n\n" .. traceback return RuntimeError.new({
error = err,
trace = debug.traceback(err, 2),
context = "Promise created at:\n\n" .. traceback
})
end end
end end
@ -83,7 +113,7 @@ local function createAdvancer(traceback, callback, resolve, reject)
if ok then if ok then
resolve(unpack(result, 1, resultLength)) resolve(unpack(result, 1, resultLength))
else else
reject(PromiseRuntimeError.new(result[1])) reject(result[1])
end end
end end
end end
@ -93,8 +123,9 @@ local function isEmpty(t)
end end
local Promise = { local Promise = {
RuntimeError = RuntimeError,
_timeEvent = RunService.Heartbeat, _timeEvent = RunService.Heartbeat,
_getTime = tick _getTime = tick,
} }
Promise.prototype = {} Promise.prototype = {}
Promise.__index = Promise.prototype Promise.__index = Promise.prototype
@ -197,7 +228,7 @@ function Promise._new(traceback, callback, parent)
) )
if not ok then if not ok then
reject(PromiseRuntimeError.new(result[1])) reject(result[1])
end end
end)() end)()
@ -225,7 +256,7 @@ function Promise.defer(callback)
local ok, _, result = runExecutor(traceback, callback, resolve, reject, onCancel) local ok, _, result = runExecutor(traceback, callback, resolve, reject, onCancel)
if not ok then if not ok then
reject(PromiseRuntimeError.new(result)) reject(result[1])
end end
end) end)
end) end)
@ -1000,19 +1031,24 @@ function Promise.prototype:_resolve(...)
self:_resolve(...) self:_resolve(...)
end, end,
function(...) function(...)
local runtimeError = chainedPromise._values[1] local maybeRuntimeError = chainedPromise._values[1]
-- Backwards compatibility <v2 -- Backwards compatibility < v2
if chainedPromise._error then if chainedPromise._error then
runtimeError = PromiseRuntimeError.new(chainedPromise._error) maybeRuntimeError = RuntimeError.new({
error = chainedPromise._error,
context = "[No stack trace available as this Promise originated from an older version of the Promise library (< v2)]"
})
end end
if PromiseRuntimeError.is(runtimeError) then if RuntimeError.is(maybeRuntimeError) then
return self:_reject(runtimeError:extend( return self:_reject(maybeRuntimeError:extend({
("The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n"):format( error = "This Promise was chained to a Promise that errored.",
trace = "",
context = ("The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n"):format(
self._source self._source
) )
)) }))
end end
self:_reject(...) self:_reject(...)

View file

@ -147,6 +147,20 @@ return function()
expect(trace:find("runPlanNode")).to.be.ok() expect(trace:find("runPlanNode")).to.be.ok()
expect(trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")).to.be.ok() expect(trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")).to.be.ok()
end) end)
it("should report errors from Promises with _error (< v2)", function()
local oldPromise = Promise.reject()
oldPromise._error = "Sample error"
local newPromise = Promise.resolve():andThenReturn(oldPromise)
expect(newPromise:getStatus()).to.equal(Promise.Status.Rejected)
local trace = tostring(newPromise._values[1])
expect(trace:find("Sample error")).to.be.ok()
expect(trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")).to.be.ok()
expect(trace:find("%[No stack trace available")).to.be.ok()
end)
end) end)
describe("Promise.defer", function() describe("Promise.defer", function()