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 <skip.potter.va@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-21 23:22:51 +02:00
committed by GitHub
parent cb294405a5
commit 5f58941db2
2 changed files with 25 additions and 8 deletions

View File

@@ -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>): 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
}

View File

@@ -370,7 +370,9 @@ async function startMessageLoop(): Promise<void> {
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<void> {
});
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