Say you are trying to ship a plugin update before a client demo, and the console throws an error you’ve never seen before — something vague about a node no longer existing, thrown from a line of code that hasn’t changed in weeks. Nothing in your recent commits explains it. The plugin worked yesterday. This is the kind of moment that eats an afternoon if you approach it by guessing, and takes ten minutes if you know which category the error belongs to.
This guide is organized around that idea. Match your symptom to the section below, check the likely cause, apply the fix, and move on. It’s meant to be scanned, not read cover to cover.
Symptom: “Node Not Found” or “Cannot Read Property of Undefined” on Nodes That Should Exist
You reference a node by ID, or hold onto a node reference across an async operation, and the plugin throws an error claiming that node doesn’t exist — even though you can see it sitting right there in the canvas.
Cause: Figma node references go stale faster than most developers expect. If a user deletes, moves, or even just interacts with the canvas while your plugin is mid-await, the node object you’re holding may no longer point to anything valid. This is especially common around figma.ui.onmessage handlers that fire after a delay, or any code that stores a node reference before an await and tries to use it afterward.
Fix: Re-fetch nodes by ID immediately before you use them, rather than trusting a reference captured earlier in the function. Wrap node access in a check — if (!node) return; — anywhere an async gap exists between capturing the reference and acting on it. For long-running operations, consider re-querying the selection at each step instead of assuming it hasn’t changed since the plugin started.
Symptom: Plugin Works in Dev Mode but Fails Silently After Publishing
Everything runs fine when you test locally through Figma’s development plugin import. After publishing, users report the plugin “does nothing” when they open it — no crash, no console error visible to them, just silence.
Cause: This is almost always a manifest or permissions mismatch that dev mode tolerates but the published environment enforces more strictly. Missing networkAccess domains, an outdated main or ui file path in manifest.json, or a permission scope that was never declared are the usual culprits. Dev mode sometimes loads cached files or ignores manifest strictness in ways the published build won’t.
Fix: Check the manifest against your actual code paths line by line — file names are a common source of drift after refactoring. Ask an affected user to open the browser console (or the equivalent in the desktop app’s developer tools) rather than relying on their description of “nothing happening,” since silent failures usually leave a trace there even if the UI shows nothing. Confirm every external domain your plugin calls is listed under networkAccess.allowedDomains; Figma blocks unlisted requests without always surfacing an obvious error to the end user.
Symptom: UI Panel Shows Blank White Screen
You open the plugin, the panel appears, and it’s just empty white space. No error banner, no content, nothing rendered.
Cause: This is typically a build or bundling problem rather than a logic error — the UI’s JavaScript bundle failed to load, threw an error before rendering anything, or the HTML file path in the manifest doesn’t match where your build tool actually outputs it. It can also happen when a UI script throws an uncaught exception on initial load, before any DOM content gets painted.
Fix: Open developer tools on the plugin window itself (right-click inside the panel, or use the Figma menu’s “Show/Hide Console” during development) and check for errors that fire immediately on load. Confirm your bundler’s output directory matches the ui path in manifest.json exactly — a stale path here is a frequent cause after switching build tools or renaming a dist folder. If you’re using a framework with hot reload, rule out a caching issue by doing a clean rebuild before assuming the code itself is broken.
Symptom: postMessage Data Isn’t Arriving Between UI and Main Thread
You send a message from the UI to the plugin’s main code, or vice versa, and the receiving side’s handler never fires — or fires with undefined where you expected a payload.
Cause: The most common cause is a mismatch in how the message is wrapped. Figma’s plugin messaging expects pluginMessage as the key when sending from the UI iframe (parent.postMessage({ pluginMessage: data }, '*')), and forgetting that wrapper — or forgetting to unwrap it on the receiving end (msg.pluginMessage versus msg) — is an easy mistake that produces no error at all, just silence.
Fix: Log the raw event object on both sides before you try to destructure anything from it. This makes the wrapping mismatch visible immediately instead of leaving you to guess at what shape the data arrived in. Standardize a message format across your plugin early — a { type, payload } shape is common — so debugging one handler tells you what to expect from all the others.
Symptom: Plugin Crashes Only on Large Files or Large Selections
Everything works fine on a test file with a handful of frames. On a real production file with hundreds of layers, the plugin hangs, times out, or crashes the tab entirely.
Cause: This is a performance ceiling problem, not a logic bug. Recursive traversal of deeply nested node trees, synchronous operations run against thousands of nodes in a loop, or repeated calls to figma.getNodeById inside a loop instead of caching results — these all scale poorly and tend to pass unnoticed in small test files precisely because the file is small.
Fix: Batch or chunk heavy operations, and yield control back to the event loop periodically using something like a small await new Promise(resolve => setTimeout(resolve, 0)) inside long loops, so the plugin doesn’t lock up the main thread entirely. Cache node lookups instead of re-querying inside a loop. If a full document traversal is unavoidable, test against a file that resembles production scale early — a plugin that runs cleanly on 50 nodes and chokes at 5,000 will typically show intermediate slowdown around 500 or so, which is the point at which most developers should have already noticed and addressed it.
Symptom: Styles or Variables Applied by the Plugin Don’t Match What Was Intended
The plugin runs without error, sets a fill or a variable binding, and the result on canvas is subtly wrong — the wrong color, a variable bound to the wrong mode, or a style applied to the wrong node in a batch.
Cause: This usually traces back to an indexing or scoping mistake rather than an API misuse — applying a style meant for node A to node B because of an off-by-one error in a loop, or binding a variable using the wrong mode ID when a file has multiple modes defined. Because there’s no thrown error, this category of bug is often the hardest to catch through console logs alone.
Fix: Add temporary console logging that prints the node name and ID alongside the value being applied, right before the assignment happens. This turns an invisible mismatch into something visible in seconds. When working with variables specifically, log the mode ID being used and cross-check it against figma.variables.getVariableCollectionById output rather than assuming the first mode in the list is the one you want.
Symptom: Plugin Throws Errors Only for Certain Users, Never for You
Bug reports come in describing a crash you cannot reproduce on your own machine, with your own test files, using the same plugin version.
Cause: Environment differences are the usual explanation — a different Figma version (desktop app versus browser), a font that isn’t available on the user’s system causing a text-node operation to fail, or a file that contains node types your plugin didn’t anticipate, like a component variant structure your test files never included.
Fix: Ask for the specific file (or a duplicate with sensitive content removed) rather than a description of what happened — reproducing the exact structure is usually faster than reasoning about it secondhand. Wrap font-dependent operations in figma.loadFontAsync with error handling rather than assuming the font is present. Log the Figma app version and platform (figma.editorType, user agent) alongside error reports so environment-specific patterns become visible across multiple reports instead of looking like isolated incidents.
A Troubleshooting Reference Table
| Symptom | Likely Cause | Fix |
|---|---|---|
| “Node not found” on valid-looking nodes | Stale node reference across async gap | Re-fetch by ID before use; guard against null |
| Works in dev, fails after publishing | Manifest or permissions mismatch | Audit manifest paths and networkAccess domains |
| Blank white UI panel | Build/bundle path mismatch or load-time crash | Check UI console; verify manifest.json paths |
| Messages not arriving between threads | Missing or mismatched pluginMessage wrapper | Log raw event objects on both sides |
| Crashes only on large files | Unbatched operations, no yielding to event loop | Chunk work, cache lookups, test at production scale |
| Wrong style/variable applied silently | Indexing or mode-scoping mistake | Log node ID and value at the point of assignment |
| Errors only on certain users’ machines | Environment differences (fonts, app version, file structure) | Reproduce with their file; log environment metadata |
Building a Habit Around This
None of these categories require exotic debugging tools — a console log placed at the right moment resolves most of them faster than stepping through a debugger would. What matters more is recognizing which category a symptom belongs to before you start investigating, since a stale-reference bug and a bundling bug look similar from a distance but need entirely different fixes.
Keep a running note of which of these seven categories your plugin has hit before. Most bugs in a maintained plugin turn out to be a repeat of something you’ve already diagnosed once, just wearing a slightly different error message the second time around.
If you’re staring at an error that doesn’t fit neatly into any of the rows above, start by isolating whether it happens with an empty test file or only with a specific one — that single check tends to point toward either a logic bug or an environment bug, which narrows the search considerably before you write a single line of debugging code.