From 5f58941db2999e04608cded214061eff8a81dd1c Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Feb 2026 23:22:51 +0200 Subject: [PATCH] fix: add .catch() handlers to fire-and-forget async calls (#221) (#355) Several async calls in the message loop and group queue are fire-and-forget without .catch() handlers. When WhatsApp disconnects or containers fail unexpectedly, these produce unhandled rejections that can crash the process. Add explicit .catch() at each call site so errors are logged with full context (groupJid, taskId) instead of crashing: - channel.setTyping() in message loop (adapted for channel abstraction) - startMessageLoop() in main() - runForGroup() and runTask() in group-queue (5 call sites) Closes #221 Co-authored-by: Naveen Jain <1779929+naveenspark@users.noreply.github.com> Co-authored-by: Skip Potter Co-authored-by: Claude Opus 4.6 --- src/group-queue.ts | 24 ++++++++++++++++++------ src/index.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/group-queue.ts b/src/group-queue.ts index f72f6a0..279a598 100644 --- a/src/group-queue.ts +++ b/src/group-queue.ts @@ -80,7 +80,9 @@ export class GroupQueue { return; } - this.runForGroup(groupJid, 'messages'); + this.runForGroup(groupJid, 'messages').catch((err) => + logger.error({ groupJid, err }, 'Unhandled error in runForGroup'), + ); } enqueueTask(groupJid: string, taskId: string, fn: () => Promise): void { @@ -116,7 +118,9 @@ export class GroupQueue { } // Run immediately - this.runTask(groupJid, { id: taskId, groupJid, fn }); + this.runTask(groupJid, { id: taskId, groupJid, fn }).catch((err) => + logger.error({ groupJid, taskId, err }, 'Unhandled error in runTask'), + ); } registerProcess(groupJid: string, proc: ChildProcess, containerName: string, groupFolder?: string): void { @@ -273,13 +277,17 @@ export class GroupQueue { // Tasks first (they won't be re-discovered from SQLite like messages) if (state.pendingTasks.length > 0) { const task = state.pendingTasks.shift()!; - this.runTask(groupJid, task); + this.runTask(groupJid, task).catch((err) => + logger.error({ groupJid, taskId: task.id, err }, 'Unhandled error in runTask (drain)'), + ); return; } // Then pending messages if (state.pendingMessages) { - this.runForGroup(groupJid, 'drain'); + this.runForGroup(groupJid, 'drain').catch((err) => + logger.error({ groupJid, err }, 'Unhandled error in runForGroup (drain)'), + ); return; } @@ -298,9 +306,13 @@ export class GroupQueue { // Prioritize tasks over messages if (state.pendingTasks.length > 0) { const task = state.pendingTasks.shift()!; - this.runTask(nextJid, task); + this.runTask(nextJid, task).catch((err) => + logger.error({ groupJid: nextJid, taskId: task.id, err }, 'Unhandled error in runTask (waiting)'), + ); } else if (state.pendingMessages) { - this.runForGroup(nextJid, 'drain'); + this.runForGroup(nextJid, 'drain').catch((err) => + logger.error({ groupJid: nextJid, err }, 'Unhandled error in runForGroup (waiting)'), + ); } // If neither pending, skip this group } diff --git a/src/index.ts b/src/index.ts index 061740f..375f9ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -370,7 +370,9 @@ async function startMessageLoop(): Promise { messagesToSend[messagesToSend.length - 1].timestamp; saveState(); // Show typing indicator while the container processes the piped message - channel.setTyping?.(chatJid, true); + channel.setTyping?.(chatJid, true)?.catch((err) => + logger.warn({ chatJid, err }, 'Failed to set typing indicator'), + ); } else { // No active container — enqueue for a new one queue.enqueueMessageCheck(chatJid); @@ -466,7 +468,10 @@ async function main(): Promise { }); queue.setProcessMessagesFn(processGroupMessages); recoverPendingMessages(); - startMessageLoop(); + startMessageLoop().catch((err) => { + logger.fatal({ err }, 'Message loop crashed unexpectedly'); + process.exit(1); + }); } // Guard: only run when executed directly, not when imported by tests