Debugging Your App
A method for finding out what is actually wrong — where each kind of code prints its errors, how to inspect an app hidden inside the Shopify admin, and a lookup table of the messages you will meet.
The first question: where does this code run?
Beginners lose hours looking for logs in the wrong place. Your app's code runs in three different places, and each prints its output somewhere different.
| Code | Runs on | Its logs appear in |
|---|---|---|
loader, action, *.server.js, jobs | Your server | The terminal running shopify app dev — or docker compose logs in production |
| Your React components | The merchant's browser | The browser's DevTools console |
| Webhook deliveries, extensions | Shopify | shopify app logs and the Dev Dashboard |
You add console.log in a loader, open the browser console, and see nothing. It did print — in your terminal, because loaders run on the server.
This single misunderstanding causes more lost time than any other. Before hunting for output, ask: server or browser?
DevTools inside the Shopify admin
Your app is an iframe inside the admin, which changes how you use the browser's developer tools.
The console
By default the console shows the Shopify admin's page, not yours. At the top of the Console tab is a context dropdown, usually reading top. Switch it to your app's frame to see your own logs and run commands in your page.
The network tab
This shows every request your page makes — the most useful debugging view there is. React Router's data requests end in .data, so filter for that:
.data
Click a request to see what was sent and, crucially, what came back. A status of 401, 404 or 500 tells you immediately which layer failed, and the response body often contains the real error message.
When "saving does nothing", open the network tab and press save. No request at all → the problem is in the browser (a button, an event). A request with an error → the problem is on the server. That one observation halves the search.
A method that always works
-
Reproduce it reliably
Find the exact steps that cause the problem every time. A bug you cannot trigger on demand, you cannot confirm you fixed.
-
Find which layer broke
Browser, your server, the database, or Shopify? The network tab and the table above usually answer this in a minute.
-
Read the actual error
The whole message, not the first few words. In a stack trace, find the first line that points at your file rather than a library.
-
Check your assumptions with real values
Log what the code actually receives. The bug is almost always a value that is not what you assumed — a string instead of a number,
undefined, a different shape. -
Change one thing, then test
Several changes at once means you will not know which one fixed it — or which one broke something else.
-
Confirm the original steps now work
Repeat step 1 exactly. Then check you did not break anything nearby.
Before anything else: is this the code that is running?
A surprising share of "impossible" bugs are this. You are reading one version of the code, and a different one is running.
- Right branch?
git branch --show-current - Right config?
shopify app info— dev app, or production? - Database client current?
npx prisma generateafter switching branches. - Server restarted after editing
.env? - In production: was the container rebuilt (
--build), or is it still the old image? - Settings change? Was
shopify app deployactually run? - Browser serving a cached page? Hard-refresh.
Thirty seconds on this list saves hours of debugging code that is not even running.
Logging that actually helps
// Useless — prints [object Object]
console.log("data: " + data);
// Useful — the whole structure, readable
console.log("[products loader] data:", JSON.stringify(data, null, 2));
- Prefix every log with where it came from —
[products loader]. With several logs flying past, you need to know which is which. - Log the whole object with
JSON.stringify(x, null, 2), not string concatenation. - Log the full Shopify response when an API call misbehaves — the answer is usually in
userErrorsor a top-levelerrorsarray you were not reading. - Remove noisy debug logs before committing. Keep a few meaningful ones, such as job failures.
console.log(session) writes the shop's access token into your logs, which are often kept for weeks and seen by more people than your database.
Log ids and shop names — session.shop — never tokens, secrets, or customers' personal details.
Your debugging tools
| Tool | Use it to |
|---|---|
npx prisma studio | See exactly what is in your database. Settles "did it save?" instantly. |
shopify app graphiql | Run an Admin API query by hand and see the real response shape. |
shopify app logs | Watch webhook deliveries and extension activity live. |
shopify app webhook trigger | Fire a webhook on demand instead of waiting for a real event. |
| Dev Dashboard monitoring | See whether Shopify sent a webhook, and how your app replied. |
| Dev Console (in a dev store's admin) | See active previews, clean them up, open the app in the dashboard. |
docker compose logs -f | Follow your production server's output. |
"It works on my machine"
Your dev setup is kinder than production in three ways, and each hides a class of bug:
| Your machine | Production | Bug it hides |
|---|---|---|
| One shop installed | Many shops | Queries missing shop — leaking data between shops (Note 04) |
| A handful of products | Hundreds or thousands | Missing pagination; lists silently cut off (Note 06) |
| Fast connection, small files | Slow connections, large photos | Upload timeouts and 401s (Note 08) |
So test the way production behaves: a second dev store, demo data, a large image, and your browser's network throttling set to a slow connection.
Debugging in production
You cannot open the merchant's browser console, so:
- Start with your server logs. Most production errors are server-side, and they are already recorded.
- Ask for specifics. The exact steps, the time it happened, a screenshot of any error.
- Reproduce on a dev store with similar data — often the difference is the size or shape of their catalogue.
- Record errors where you can find them — a
lastErrorcolumn for jobs, logged failures with the shop name. You cannot debug what nobody wrote down.
Wrapping a failing call in a retry, or catching an error and returning success, makes the symptom disappear while the cause stays. The bug returns later, harder to trace.
Only add a retry once you understand why it fails and know that retrying is genuinely correct — as it is for rate limiting (Note 06).
Error lookup table
The messages you are most likely to meet, and where the explanation is.
| You see | Usually means | Note |
|---|---|---|
401 Unauthorized on a big upload | Session token expired during a slow request | 08 |
401 everywhere | Opened outside the admin, or the shop uninstalled | 04 |
App Bridge … missing required configuration fields: shop | Opened the app URL directly, not through the admin | 04 |
Unexpected token '<' … JSON | You fetched a page route and got HTML | 03 |
db.x is undefined | Stale Prisma client | 02, 05 |
Data too long for column | URL in a short String — needs @db.Text | 05 |
Unique constraint failed | Duplicate of a unique value — use upsert | 05 |
Record to update not found | update/delete matched nothing | 05 |
| Save "succeeds" but nothing changes | Unread userErrors | 06 |
| Only some items show up | Missing pagination, or picker truncation | 06, 09 |
| Form fields arrive empty | Missing name, or Polaris fields not submitted manually | 07 |
| Components render as blank space | Polaris script not loaded on that page | 09 |
| Webhook fires repeatedly | Handler too slow, or returning an error | 10 |
| Change not live in production | Only one of the two deploys was done | 11 |
| New permission does nothing | Merchant has not approved the new scopes | 12 |
| Paying merchant sent to pricing page | A failed subscription check treated as "unpaid" | 16 |
| App embed does nothing | Embed not switched on by the merchant | 17 |
When you are truly stuck
- Explain the problem out loud, step by step, to a colleague or even to nobody. You will often spot the wrong assumption mid-sentence.
- Make the smallest version that still fails. Strip away everything unrelated until the bug has nowhere to hide.
- Take a break. Genuinely. Fresh eyes find in five minutes what tired ones missed for an hour.
- When you ask for help, include: what you expected, what happened instead, the exact error, and what you have already tried.
Cheat sheet
# where are my logs?
loader / action / jobs → terminal (dev) · docker compose logs (prod)
components → browser console — switch context to your frame
webhooks / extensions → shopify app logs · Dev Dashboard
# first checks
network tab, filter ".data" no request = browser · error = server
is this the code running? branch · config · prisma generate · rebuild
# log well
console.log("[where]", JSON.stringify(x, null, 2))
never log tokens, secrets or personal data
# method
reproduce → find the layer → read the whole error
→ check real values → change one thing → confirm