From b62327782f5210de32222be707753eeb8d1a049c Mon Sep 17 00:00:00 2001 From: MrCooper42 Date: Thu, 6 Aug 2026 15:07:51 -0600 Subject: [PATCH] fix(wrappers/cron): don't leave an unhandled rejection when a bot promise rejects When a bot handler returns a promise, the wrapper attached both `promise.then(...)` and `promise.catch(...)` to the *same* promise. Those are siblings, not a chain, so a rejection produced two outcomes: - the `.catch()` handled it correctly and reported the bot complete with status "error"; - the `.then()`-derived chain also rejected, with no handler. Node's default unhandled-rejection mode is `throw`, and the wrapper removes Lambda's own `uncaughtException` listener without registering an `unhandledRejection` one, so that orphaned chain terminated the process *after* the bot had already been reported complete. On Lambda this surfaces as Runtime.UnhandledPromiseRejection / Runtime.ExitError. Pass the rejection handler as the second argument to `then()` instead. That gives a rejection exactly one handler and leaves no orphaned chain, while preserving existing behaviour for both branches: - `botHandler.length < 3` keeps the success path plus error reporting; - 3-arg (callback-style) handlers still only get the rejection path, since they report their own success through the callback. Using `then(onFulfilled, onRejected)` rather than `.then().catch()` is deliberate: it keeps `onRejected` scoped to a rejection of the handler's promise, so a throw inside the success path is not newly rerouted into the error branch. Co-Authored-By: Claude Opus 5 (1M context) --- wrappers/cron.js | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/wrappers/cron.js b/wrappers/cron.js index 162637f..d4bcddd 100644 --- a/wrappers/cron.js +++ b/wrappers/cron.js @@ -180,24 +180,34 @@ module.exports = function(configOverride, botHandler) { callback(null, err || data); }); }); - if (promise && typeof promise.then == "function" && botHandler.length < 3) { - promise.then(data => { - cmdLogger.log("[LEOCRON]:complete:" + cronkey); - cron.reportComplete(event.__cron, context.awsRequestId, err ? "error" : "complete", err ? err : '', {}, function(err2, data2) { - if (err || err2) { - logger.log(err || err2); - } - callback(null, err || data); - }); - }); - } - if (promise && typeof promise.catch == "function") { - promise.catch(err => { - cmdLogger.log("[LEOCRON]:complete:" + cronkey); - cron.reportComplete(event.__cron, context.awsRequestId, "error", err, {}, function() { - callback(null, err); - }); + // A rejection needs exactly one handler. Attaching .then() and .catch() + // to the same promise made them siblings rather than a chain: on + // rejection the .catch() reported the bot complete, but the + // .then()-derived chain stayed unhandled, and Node's default + // unhandledRejection mode ("throw") then killed the process after the + // bot had already been reported complete. Passing the rejection + // handler as then()'s second argument keeps the previous behaviour of + // both branches while leaving no unhandled chain behind. + let onRejected = err => { + cmdLogger.log("[LEOCRON]:complete:" + cronkey); + cron.reportComplete(event.__cron, context.awsRequestId, "error", err, {}, function() { + callback(null, err); }); + }; + if (promise && typeof promise.then == "function") { + if (botHandler.length < 3) { + promise.then(data => { + cmdLogger.log("[LEOCRON]:complete:" + cronkey); + cron.reportComplete(event.__cron, context.awsRequestId, err ? "error" : "complete", err ? err : '', {}, function(err2, data2) { + if (err || err2) { + logger.log(err || err2); + } + callback(null, err || data); + }); + }, onRejected); + } else if (typeof promise.catch == "function") { + promise.catch(onRejected); + } } }).catch(err => { cron.reportComplete(event.__cron, context.awsRequestId, "error", err, {}, function() {