Skip to content

fix: skip failing contacts in campaign batch instead of halting#411

Closed
tejassinghbhati wants to merge 1 commit into
usesend:mainfrom
tejassinghbhati:fix/campaign-batch-skip-failing-contact
Closed

fix: skip failing contacts in campaign batch instead of halting#411
tejassinghbhati wants to merge 1 commit into
usesend:mainfrom
tejassinghbhati:fix/campaign-batch-skip-failing-contact

Conversation

@tejassinghbhati

@tejassinghbhati tejassinghbhati commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Closes #408

Problem

The campaign batch worker had no per-contact error handling. If processContactEmail threw for any single contact (bad template variables, missing region queue, etc.), the entire for loop exited immediately — before the cursor update ran. The campaign would stay RUNNING with lastCursor frozen, and the scheduler would re-enqueue the same batch every ~1.5s, hitting the same failing contact in an infinite loop.

Fix

Wrapped processContactEmail in a try/catch inside the loop. A failing contact is now logged and skipped; the loop continues to the remaining contacts and the cursor advances normally at the end.

What changes

  • apps/web/src/server/service/campaign-service.ts — added try/catch around processContactEmail call in the batch worker loop

Summary by cubic

Prevent campaign batches from halting on a single failing contact by skipping errors and continuing, so campaigns progress and the scheduler stops re-enqueuing the same batch.

  • Bug Fixes
    • Wrapped processContactEmail in a try/catch inside the per-contact loop.
    • On error, log with contactId and campaignId, then skip to the next contact.
    • Cursor now advances after the loop, avoiding stuck RUNNING state and infinite re-enqueue.

Written for commit 306a9dd. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved batch processing reliability by skipping failed contacts instead of stopping the entire campaign run.
    • Added clearer error handling so issues with one contact do not impact the rest of the batch.

@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

@tejassinghbhati is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

In CampaignBatchService, the worker loop that iterates over contacts in a batch now wraps the processContactEmail(...) call in a try/catch block. When an exception is thrown for a given contact, the error is logged with the err, contactId, and campaignId identifiers, and the loop advances to the next contact. Previously, any exception propagated out of the loop, halting the entire batch and preventing the cursor from advancing.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly matches the main fix: skipping failing contacts so campaign batches keep running.
Linked Issues check ✅ Passed The change matches #408 by catching per-contact failures, logging them, and allowing the batch to continue so the cursor can advance.
Out of Scope Changes check ✅ Passed No unrelated code changes are indicated beyond the campaign batch error-handling fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/server/service/campaign-service.ts`:
- Around line 1037-1060: The catch in campaign-service’s contact processing path
is too broad and currently swallows systemic failures while the batch still
advances lastCursor. In processContactEmail handling inside the contact loop,
keep skip-and-continue only for explicitly contact-scoped, non-retriable errors,
and rethrow all other exceptions so the surrounding batch can retry safely
instead of permanently skipping contacts. Use the existing logger.error block
for the non-retriable path, and update the logic around the campaign/contact
processing flow so lastCursor only advances when the batch is safe to commit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8b85a09-bf70-447a-b4fc-2e552e2229ea

📥 Commits

Reviewing files that changed from the base of the PR and between d2dc18f and 306a9dd.

📒 Files selected for processing (1)
  • apps/web/src/server/service/campaign-service.ts

Comment on lines +1037 to +1060
try {
await processContactEmail({
contact,
campaign,
allowedVariables,
emailConfig: {
from: campaign.from,
subject: campaign.subject,
replyTo: Array.isArray(campaign.replyTo) ? campaign.replyTo : [],
cc: Array.isArray(campaign.cc) ? campaign.cc : [],
bcc: Array.isArray(campaign.bcc) ? campaign.bcc : [],
teamId: campaign.teamId,
campaignId: campaign.id,
previewText: campaign.previewText ?? undefined,
domainId: domain.id,
region: domain.region,
},
});
} catch (err) {
logger.error(
{ err, contactId: contact.id, campaignId },
"Failed to process contact; skipping to next",
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not swallow systemic failures while still advancing lastCursor

Line 1037 and Line 1055 currently catch all exceptions and continue, so transient infra failures (DB/Redis/queue) can skip contacts permanently once the cursor advances at Line 1065. Skip-and-continue should be limited to explicitly contact-scoped, non-retriable errors; rethrow the rest so the batch can retry safely.

Suggested direction
+      const isSkippableContactError = (err: unknown) =>
+        err instanceof TemplateVariableError ||
+        err instanceof MissingContactDataError;
+
       for (const contact of contacts) {
         if (existingSet.has(contact.id)) continue;
 
         try {
           await processContactEmail({
             // ...
           });
         } catch (err) {
           logger.error(
             { err, contactId: contact.id, campaignId },
             "Failed to process contact; skipping to next",
           );
+          if (!isSkippableContactError(err)) throw err;
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await processContactEmail({
contact,
campaign,
allowedVariables,
emailConfig: {
from: campaign.from,
subject: campaign.subject,
replyTo: Array.isArray(campaign.replyTo) ? campaign.replyTo : [],
cc: Array.isArray(campaign.cc) ? campaign.cc : [],
bcc: Array.isArray(campaign.bcc) ? campaign.bcc : [],
teamId: campaign.teamId,
campaignId: campaign.id,
previewText: campaign.previewText ?? undefined,
domainId: domain.id,
region: domain.region,
},
});
} catch (err) {
logger.error(
{ err, contactId: contact.id, campaignId },
"Failed to process contact; skipping to next",
);
}
const isSkippableContactError = (err: unknown) =>
err instanceof TemplateVariableError ||
err instanceof MissingContactDataError;
try {
await processContactEmail({
contact,
campaign,
allowedVariables,
emailConfig: {
from: campaign.from,
subject: campaign.subject,
replyTo: Array.isArray(campaign.replyTo) ? campaign.replyTo : [],
cc: Array.isArray(campaign.cc) ? campaign.cc : [],
bcc: Array.isArray(campaign.bcc) ? campaign.bcc : [],
teamId: campaign.teamId,
campaignId: campaign.id,
previewText: campaign.previewText ?? undefined,
domainId: domain.id,
region: domain.region,
},
});
} catch (err) {
logger.error(
{ err, contactId: contact.id, campaignId },
"Failed to process contact; skipping to next",
);
if (!isSkippableContactError(err)) throw err;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/server/service/campaign-service.ts` around lines 1037 - 1060,
The catch in campaign-service’s contact processing path is too broad and
currently swallows systemic failures while the batch still advances lastCursor.
In processContactEmail handling inside the contact loop, keep skip-and-continue
only for explicitly contact-scoped, non-retriable errors, and rethrow all other
exceptions so the surrounding batch can retry safely instead of permanently
skipping contacts. Use the existing logger.error block for the non-retriable
path, and update the logic around the campaign/contact processing flow so
lastCursor only advances when the batch is safe to commit.

@KMKoushik

Copy link
Copy Markdown
Member

ty, took the commit and worked on top of it

@KMKoushik KMKoushik closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞 - Campaign halts after first email — one failing contact wedges the whole batch permanently

2 participants