A retail bank sends an appointment booking email every quarter. The adviser name and diary link are read from a reference table, keyed on the customer branch code. In QA, every test branch has an adviser, and the email passes validation without comment.
The day before the send, a developer adds a safeguard. If the adviser cannot be found, the code calls RaiseError("Conseiller introuvable"). The intent is sound, nobody wants an email greeting a nameless adviser.
The next morning, the send of 400,000 emails stops after a few minutes, in error. Three branches opened the previous month had no adviser in the reference table yet. And nobody knows how many customers are affected, or which ones.
The function takes one mandatory parameter and four optional ones. The second is the one missing from the incident.
RaiseError(errorMessage, boolSkipCurrentOnly, apiErrorCode, apiErrorNumber, boolPreserveDataExt)
| Parameter | Type | Role |
|---|---|---|
errorMessage | String, mandatory | Message attached to the error |
boolSkipCurrentOnly | Boolean, false by default | true skips the current recipient and lets the send carry on, false stops the whole job |
apiErrorCode | String | Free-form error code, useful for API calls |
apiErrorNumber | Number | Free-form error number |
boolPreserveDataExt | Boolean | true keeps the data extension writes made before the error, false rolls them back |
A call with a single parameter is an emergency stop, then, not a filter. The confusion is easy to make, because the code reads like a safety condition while it behaves like a fire alarm.
⚠️ The QA trap: a safeguard that never fires in QA has not been tested. Your test data sets hold clean customers, and that is exactly what stops anyone noticing the difference between the two modes.
Before writing a single line, sort every piece of data in the email by what should happen when it is missing. The sorting is done once, and it holds for every email in the campaign.
| Situation | Example | Answer |
|---|---|---|
| Nice-to-have data | First name missing | Fallback content with If Empty(...) |
| Data this recipient cannot do without | No adviser found for their branch | Skip and log, RaiseError(msg, true, ...) |
| Anomaly affecting the whole send | Campaign switched off, reference table empty | Stop the send, RaiseError(msg, false) |
| Population known in advance | Customers with no branch attached | Exclude upstream with a SQL query |
The last line counts as much as the others. Salesforce recommends keeping the function for error handling rather than segmentation. If you know before the send that a population has to be dropped, removing it with a query costs less than evaluating it recipient by recipient during rendering, and the targeted volume shown before the send becomes accurate again.
The order of the blocks mirrors the order of the decisions. The first block in the email checks what affects everybody, campaign parameters and reference tables. The second drops incomplete recipients. Only the third adapts the copy.
%%[
Var @actif
Set @actif = Lookup("Parametres_Campagne", "Actif", "CodeCampagne", "RDV_T3")
If Empty(@actif) Then
RaiseError("Parametres_Campagne : ligne RDV_T3 absente", false)
EndIf
]%%
That check runs for every recipient, but it is the first one that triggers the stop. When the condition rests on a date, call Now(true) rather than Now(): the function returns the job start time, the same value for everyone, and the result does not shift mid-send. Write the messages without accents, they are meant for support teams and technical logs.
Skipping a recipient without knowing which one solves half the problem. The answer is one line written to a data extension just before the error is raised.
%%[
Var @cle, @job, @agence, @conseiller
Set @cle = _subscriberkey
Set @job = jobid
Set @agence = Trim(AttributeValue("CodeAgence"))
If not Empty(@agence) Then
Set @conseiller = Lookup("Conseillers", "NomConseiller", "CodeAgence", @agence)
EndIf
If Empty(@conseiller) Then
UpsertDE("Journal_Abonnes_Ignores", 3,
"SubscriberKey", @cle,
"JobID", @job,
"Motif", "CONSEILLER_ABSENT",
"Detail", Concat("Agence : ", @agence))
RaiseError("Conseiller introuvable", true, "CONSEILLER_ABSENT", 1001, true)
EndIf
]%%
Three details separate a full log from an empty one. The write comes before the call, because the code stops at the error and never reaches the next line. The fifth parameter is set to true, otherwise the write is rolled back with everything else. And UpsertDE is used rather than InsertDE: the AMPscript of a send is processed in batches at the last step, the same recipient can be rendered more than once, and the upsert avoids a failure on a row that already exists.
The second argument of UpsertDE declares how many search pairs follow, three here. If that number does not match the pairs supplied, the function throws an exception. Read it again on every change to the block, it is the most common mistake on this function.
The day after the send, one query gives the count per branch, ready to pass to the team that owns the adviser table.
SELECT
j.Detail,
COUNT(*) AS NbClients
FROM Journal_Abonnes_Ignores AS j
WHERE j.Motif = 'CONSEILLER_ABSENT'
AND j.DateTrace >= DATEADD(DAY, -1, GETDATE())
GROUP BY j.Detail
Set a retention rule on that log the day you create it. It holds nothing but technical identifiers and a reason, yet it is not meant to grow for ever, and a few months is ample to work through the gaps found after a send.
💡 Send log or dedicated data extension: the send log is an account-level data extension populated automatically on every send. Salesforce advises keeping it to roughly ten custom fields and a short retention, in the region of ten days. For a focused log of skipped recipients, a dedicated table stays easier to query.
A skipped recipient does not vanish from the system, they simply change status in several places. The documentation spells out three effects that teams tend to discover too late.
That last point deserves attention when a journey chains several messages on the same piece of data. Dropping a customer from the first email does not protect them from the second. Either you repeat the check in each send, or you set an exit condition in the journey itself.
On how visible the error message is, stay cautious. It serves technical logs and support above all, and it is hard to find in the interface. Write it without accents and keep the same code in the log and in the third parameter; the matching will happen through that code, not through the sentence.
In Adobe Campaign, exclusions happen in the workflow or through typology rules applied when the delivery is analysed. The result is a readable exclusion report, where every dropped recipient appears with a reason. You open the analysis, you see the volumes, you decide whether to deliver.
Marketing Cloud offers no such view. The log built above plays the part of that exclusion report, and it falls to you to build it before the first send, not after the first incident. As for the global stop, it resembles a control rule that blocks a delivery, except that it fires during the send rather than before it. A stop at six in the morning with no notification will only be found when the team arrives, so plan your Automation Studio alerts accordingly.
Before publishing an email that calls this function, open the code and count the parameters. One parameter means the first incomplete recipient stops the entire send. Then create a test data extension with one row per branch of the code, nominal case, missing nice-to-have data, missing required data, and check every branch in preview before scheduling anything.
Because the second parameter, boolSkipCurrentOnly, is false by default. A call with a single parameter is therefore an emergency stop rather than a filter: the first incomplete recipient stops the whole job. Set to true, the function skips the current recipient and lets the send carry on.
You write a row to a data extension just before raising the error, because the code stops at the error and never reaches the next line. Use UpsertDE rather than InsertDE, and set the fifth parameter of RaiseError to true, otherwise the write is rolled back with everything else. The day after the send, a query on that log gives the count per branch.
No, they are not counted towards billed consumption. They do appear in tracking and reporting figures, which explains the gap between the targeted volume and the sent volume.
No. In a journey the contact is removed from one send only: they carry on through the journey and will receive the later emails, which therefore need safeguards of their own. Either you repeat the check in each send, or you set an exit condition in the journey itself.
No. Salesforce recommends keeping the function for error handling rather than segmentation. If you know before the send that a population has to be dropped, removing it with a query costs less than evaluating it recipient by recipient during rendering, and the targeted volume shown before the send becomes accurate again.
I can review your personalisation blocks, your exclusions and your send logs, then send you a written diagnosis with the fixes to apply.
Describe your context →