diff --git a/lib/init.lua b/lib/init.lua index a0ee2d4..5d79057 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -222,6 +222,7 @@ local Promise = { Status = makeEnum("Promise.Status", { "Started", "Resolved", "Rejected", "Cancelled" }), _getTime = os.clock, _timeEvent = game:GetService("RunService").Heartbeat, + _unhandledRejectionCallbacks = {}, } Promise.prototype = {} Promise.__index = Promise.prototype @@ -1809,6 +1810,10 @@ function Promise.prototype:_reject(...) -- Build a reasonable message local message = string.format("Unhandled Promise rejection:\n\n%s\n\n%s", err, self._source) + for _, callback in ipairs(Promise._unhandledRejectionCallbacks) do + task.spawn(callback, self, unpack(self._values, 1, self._valuesLength)) + end + if Promise.TEST then -- Don't spam output when we're running tests. return @@ -1986,4 +1991,25 @@ function Promise.fromEvent(event, predicate) end) end +--[=[ + Registers a callback that runs when an unhandled rejection happens. An unhandled rejection happens when a Promise + is rejected, and the rejection is not observed with `:catch`. + + The callback is called with the actual promise that rejected, followed by the rejection values. + + @param callback (promise: Promise, ...: any) -- A callback that runs when an unhandled rejection happens. + @return () -> () -- Function that unregisters the `callback` when called +]=] +function Promise.onUnhandledRejection(callback) + table.insert(Promise._unhandledRejectionCallbacks, callback) + + return function() + local index = table.find(Promise._unhandledRejectionCallbacks, callback) + + if index then + table.remove(Promise._unhandledRejectionCallbacks, index) + end + end +end + return Promise diff --git a/lib/init.spec.lua b/lib/init.spec.lua index 3168cf8..7b86d44 100644 --- a/lib/init.spec.lua +++ b/lib/init.spec.lua @@ -35,6 +35,40 @@ return function() end) end) + describe("Unhandled rejection signal", function() + it("should call unhandled rejection callbacks", function() + local badPromise = Promise.new(function(_resolve, reject) + reject(1, 2) + end) + + local callCount = 0 + + local function callback(promise, rejectionA, rejectionB) + callCount += 1 + + expect(promise).to.equal(badPromise) + expect(rejectionA).to.equal(1) + expect(rejectionB).to.equal(2) + end + + local unregister = Promise.onUnhandledRejection(callback) + + advanceTime() + + expect(callCount).to.equal(1) + + unregister() + + Promise.new(function(_resolve, reject) + reject(3, 4) + end) + + advanceTime() + + expect(callCount).to.equal(1) + end) + end) + describe("Promise.new", function() it("should instantiate with a callback", function() local promise = Promise.new(function() end)