An Error With a Number on It
When a message to five people fails for two of them, the useful information is which two. Most integrations throw that away and report one reason. This one keeps the number on the front of every error, and the difference is a minute of work against an afternoon of it.
There is a moment in every operations job where somebody says the alerts are not working. What follows depends entirely on what the system recorded. If it recorded that a send failed, the next hour is spent establishing whether it failed for everybody or for one person, which is the question that decides whether anything is actually wrong. If it recorded which numbers failed and why, the conversation is over in about ninety seconds.
The number goes on the front of the error
Every failure in this connector is recorded as the recipient, then the reason. Not a count, not the last one — each failure, with the number it belongs to, and all of them returned together.
It costs one string concatenation per failure and it changes the character of the diagnosis entirely.
| What is reported | What it tells you | What you do next |
|---|---|---|
| Sending failed | Something is wrong | Start investigating from nothing |
| Two of five failed | It is not systemic | Find out which two |
| These two numbers failed, with reasons | Exactly what is wrong and where | Fix the two numbers |
Every row down that table is one more piece of context the system already had at the moment of failure and chose whether to keep.
That is the thing worth internalising. None of this is extra work at diagnosis time — it is a decision made at the point of failure, when the information is free, about whether to carry it or discard it. Discarding is the default because it requires writing nothing.
Why this connector can do it and the others do not
An honest comparison, since two other connectors in this series also send to a list and both keep only the last reason.
The difference is what the provider offers. Twilio takes one request per recipient, so each has its own response and its own error, and keeping them separate is natural. The other two either take the whole list in a single call or return a shape where the reasons are not individually attributable in the same way.
So this is not a case of one connector being written more carefully than another. It is a case of the provider's interface making a certain kind of honesty cheap, and the connector taking it. Where a provider gives you per-recipient detail, keep it. Where it does not, do not invent it.
One failure costs one send
The loop's error handling has a detail that is easy to write the wrong way and produces a much worse failure.
When a request fails at the transport level — a refused connection, a timeout, a network blip — the failure is caught, recorded against that recipient, and the loop moves on to the next.
Written the other way, with one guard around the whole loop, a single unreachable moment on the second of eight recipients abandons the remaining six. The count would show one sent and one failed, and six people would simply never be considered — with nothing in the record to say so.
The narrow guard costs a few characters. It is the same shape as the notification fan-out, where each destination is guarded on its own so a jammed door does not block the corridor, and it recurs in this codebase because it recurs in the problem.
Two errors joined, and the reason they are
All the collected failures are joined into one reported message rather than reported as a list.
That is a compromise with the shape of the surrounding system: the result travels back through a settings screen and a log line, both of which take a string. A structured list would be better and would need every consumer to understand it.
What matters is that the information survives to the place a person reads. A joined string containing two numbers and two reasons answers the question. A count would not, and neither would the last reason on its own.
It is worth being clear that this is a trade rather than an ideal. The right amount of structure is however much survives to the reader, and on this path a string does.
And an empty list is still not a success
The overall verdict requires two things: at least one message actually sent, and no failures recorded.
The first half is the one that catches the quiet case. A connector configured, enabled and pointed at an empty recipient list produces no sends and no failures — and a verdict testing only for the absence of errors would call that a success.
It is the same rule as the other fan-out connectors, and it is worth stating every time because it is the failure mode that survives longest. Nothing errors. Nothing arrives. There is no evidence, only an absence, and absences are not what anybody monitors.
The list is also cleaned before any of this — split on commas and line breaks, trimmed, empties dropped — so a trailing comma in a settings field does not become a send to nothing and an error from a provider about a malformed number.
What is in place, layer by layer
How a Twilio send reports itself
The recipient on the front of every error
Each failure is recorded as the number and then the reason, so the diagnosis names the destination rather than the batch.
All failures kept, not just the last
Every recipient that did not go is reported, because two failures with different causes are two different problems.
One request per recipient
Each destination is its own send, so a bad number cannot take the rest of the list with it.
Transport failures caught per send
A refused connection costs one recipient and the loop continues, rather than abandoning everybody after the failure.
The provider's own message extracted
Twilio's human-readable field is read where present, with the raw response as a fallback, so the reason is the one in their documentation.
A strict verdict with a floor
Success requires at least one send and no failures, so an empty recipient list is reported rather than passing as fine.
The list cleaned before use
Split on commas and line breaks, trimmed, empties dropped — so a trailing comma is not an attempt to message nothing.
A bounded wait per recipient
Each request is individually capped, so a slow response part-way down a list does not stall the sends behind it.
The other two SMS providers take a whole list in one call, so this level of per-recipient detail is available here and not there. The connector keeps what its provider offers rather than inventing what it does not.
Three positions held on purpose
- Context is kept at the moment of failure rather than reconstructed afterwards. Which recipient failed is free to record and impossible to recover, and the default is to lose it because losing it requires writing nothing.
- Per-recipient detail is kept where the provider offers it and not invented where it does not. A connector that reported per-number reasons on a provider that returns one summary would be presenting a guess as a fact.
- A send to nobody is a failure. Requiring at least one actual send in the verdict is what turns an empty recipient list from a silent success into something a person sees.
Five questions about how failures are reported
Which recipient failed?
A good answer sounds like
It says.
What ours actually is
Every error carries its number on the front, and all of them are reported.
What if several fail for different reasons?
A good answer sounds like
All the reasons.
What ours actually is
Each is kept with its recipient rather than collapsed to the most recent one.
Does one network failure stop the rest?
A good answer sounds like
No.
What ours actually is
Caught per send, so a refused connection costs that recipient and the loop continues.
Is an empty recipient list a success?
A good answer sounds like
No.
What ours actually is
Success requires at least one actual send, not merely the absence of errors.
Where does the reason text come from?
A good answer sounds like
The provider.
What ours actually is
Twilio's own message field, with the raw response as a fallback.
Our take
Almost every hard-to-diagnose production problem is a place where the system knew something at the moment it mattered and did not write it down. The failing record's identifier, the recipient, the parameter the provider objected to — all of it is free to keep at the point of failure and unrecoverable an hour later. The discipline is small and it has to be applied at the time: when you are writing the line that records a failure, ask what a person reading it at nine on a Monday will need to know, and put that in. It is never more than a few characters, and it is the whole difference between a support conversation and an investigation.
Alerts that can tell you who did not get them
A notification system is only as good as its account of what did not arrive. That account is written at the moment of failure or not at all.
Talk through SMS alertsFrequently asked questions
If one number is wrong, do the others still receive the message?
Yes. Each recipient is a separate send, so a bad number affects only itself. The result reports which number failed and why, alongside the count that went.
Why does the report show several errors joined together?
Because several recipients failed and each reason is kept. The result travels through a settings screen and a log line, both of which take text, so the failures are joined rather than structured — the aim is that the information survives to the person reading it.
Do the other SMS providers report per-recipient errors?
They report differently, because they work differently — the other two take a whole list in a single call rather than one request per recipient. The connectors keep the detail their provider actually offers rather than presenting a guess as a fact.
What does a Twilio error usually mean?
Most often an unreachable or malformed number, a sender not permitted to message that destination, or an account limit. Twilio's own message names which, and it is passed through unchanged so it matches their documentation.
Will failed recipients be retried?
Where the send runs as background work the usual retry rules apply to the send as a whole. The bias is toward reporting clearly rather than retrying hard — the record in your workspace is the authority, and a permanently bad number is not improved by attempting it three more times.