Error handling in Shopify integrations: the part that decides everything
Integrations do not fail because the code was wrong. They fail because nobody decided what happens when a call fails at 3am.
8 min read · Integrations ·
The integrations that fail aren't the ones with clever code. They're the ones where nobody decided what happens when a call fails at three in the morning — so it fails silently, and someone notices a fortnight later that finance is short forty orders.
Error handling is not a phase after the feature. It's most of the feature.
Retry, with backoff
Not every failure is permanent. Distinguish:
- Transient — a timeout, a 429 rate limit, a 502. Retry these.
- Permanent — a 400 because the payload is invalid, a 404 because the record doesn't exist. Retrying achieves nothing.
Retry the transient ones with exponential backoff and jitter: wait 1s, 2s, 4s, 8s, with a random offset so a hundred failed jobs don't retry in unison and cause the outage again.
Cap the attempts. Something that has failed six times over an hour is not going to succeed on the seventh; it needs a human.
Idempotency is non-negotiable
Retries mean the same operation runs more than once. Shopify webhooks are delivered at least once, so the same event arrives twice regardless.
Every operation must produce the same result whether it runs once or five times:
- Key on the source record's ID, not on a generated one.
- Upsert rather than insert.
- Record processed webhook IDs and skip repeats.
- Use the target system's idempotency mechanism where it has one.
Without this you get duplicate orders in the ERP, duplicate emails and duplicate invoices — and duplicates are always discovered by someone downstream, never by the integration.
Dead letter everything that gives up
When retries are exhausted, the record goes somewhere — a table, a queue, anywhere it can be seen and replayed. Not a log line.
A dead letter store needs three things: the original payload, the error, and a way to retry it after someone fixes the cause. That last one is what turns a 2am failure into a five-minute morning task rather than a manual re-entry job.
Partial failures are the hard case
A batch of 500 product updates where 12 fail. Two wrong answers: fail the whole batch (497 good updates lost), or report success (12 products quietly wrong forever).
The right answer: process what you can, record what you couldn't, report both. This is exactly why bulk operations return a results file — an operation marked COMPLETED can contain thousands of individual failures, and code that only checks the operation status misses all of them.
The same applies to GraphQL mutations, which return HTTP 200 with userErrors populated. Select userErrors and act on it. This is the single most common bug in first Shopify integrations — the Admin API guide.
Rate limits are a design input
Shopify's Admin API limits by calculated query cost, and every response tells you your remaining budget. Don't fire until throttled and then back off — read throttleStatus and pace inside the bucket.
An integration that treats rate limiting as an error condition rather than a normal operating constraint will fail predictably on your largest catalogue, at your busiest time.
Alert on the right things
Alerting on every error trains everyone to ignore alerts. Alert on:
- A rising failure rate, not individual failures.
- The dead letter queue growing, which means something systematic broke.
- A sync that didn't run, which is invisible without an explicit check.
- The reconciliation report showing drift — reconciliation.
- Anything touching money or orders failing at all.
And send them somewhere a person actually looks. An alert into an unwatched channel is a log entry with extra steps.
Make failures visible in Shopify
The best thing you can build for the client, and the thing most often left out: an admin block on the order page showing whether that order reached the external system, when, and with what result — plus an action to retry it.
That turns "the order didn't go through, email the developer" into something support staff resolve themselves. It's a day of work and it changes how the integration is experienced entirely.
Log enough to answer questions
For anything touching orders or money, log the request, the response, the timestamp and the correlation ID. When someone asks in three months why order 4471 never reached the ERP, that log is the only way to answer.
Don't log credentials or access tokens — check your error handler isn't serialising whole request objects, which is how tokens end up in log files.
The checklist
- Classify errors: transient versus permanent.
- Exponential backoff with jitter, capped.
- Idempotent operations, keyed on source IDs.
- Dead letter store with replay.
- Partial failures recorded, not swallowed.
userErrorschecked on every mutation.- Rate limits paced, not hit.
- Alerts on rates and on silence.
- Failures visible in the Shopify admin.
- Logs sufficient to answer "what happened to order X".
Error handling is the difference between an integration you own and one that owns you. It's also the line item clients most want removed from a quote.
Is this the problem you’re looking at?
Send me the link to your store and a line about what is going wrong. You get a straight answer within one business day — no pitch, no obligation.
[email protected]Or see what I do around Shopify: services, work beyond the theme, selected work.