LEARN · DEBUGGING GUIDE

Fastify Plugin Not Registered: Debugging the 'Missing Plugin' Error

The 'plugin not registered' error in Fastify almost always means your plugin wasn't registered before it was used, or it was registered in a different scope. Here's how to find and fix it in minutes.

IntermediateNode.js7 min read

What this usually means

Fastify's plugin system enforces encapsulation and dependency ordering. When you see 'plugin not registered', it usually means one of three things: (1) You registered the plugin after you tried to use its decorators or routes, (2) You registered the plugin inside a child scope but tried to access its features from a sibling or parent scope (encapsulation violation), or (3) You have a circular dependency or double registration. Fastify's plugin graph is strict: you must register every plugin exactly once, before any route or decorator that depends on it, and in the correct parent-child hierarchy.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check the exact order of register() calls in your entry file (app.js or server.js) — the plugin must be registered before any route that uses it.
  • 2Search for any duplicate register() calls for the same plugin across your codebase: grep -r 'register(.*pluginName' src/.
  • 3Verify that the plugin is exported as a fastify-plugin wrapper: check for wrap = require('fastify-plugin') and module.exports = wrap(function (fastify, opts, done) { ... }).
  • 4If using async/await, ensure you await fastify.register() before calling fastify.listen() or accessing decorators.
  • 5Log the plugin names right after registration: fastify.printPlugins() to see the registration order and hierarchy.
  • 6Temporarily remove all .after() and .ready() hooks to rule out race conditions.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchYour main server entry file (e.g., app.js, server.js, index.js) — where register() calls are made.
  • searchThe plugin file itself — verify it uses fastify-plugin wrapper and exports correctly.
  • searchpackage.json — check for duplicate versions of the same plugin causing double registration.
  • searchNode_modules cache: rm -rf node_modules && npm install to clear any stale plugin versions.
  • searchFastify's printed plugin tree by calling fastify.printPlugins() at startup.
  • searchAny .after() or .ready() callbacks that might be accessing plugin decorators before registration completes.
  • searchThe error stack trace — it often points to the exact line where the missing decorator is accessed.
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningFastify-plugin wrapper missing: the plugin is a regular function instead of using fastify-plugin.
  • warningAsync registration without await: fastify.register() returns a Promise that must be awaited before using the plugin's features.
  • warningPlugin registered inside a child plugin's scope but accessed in the parent (encapsulation violation).
  • warningDouble registration due to hot-reload or dynamic imports registering the same plugin twice.
  • warningCircular dependency between plugins causing one to not be registered before the other is used.
  • warningPlugin registered after fastify.listen() — post-listen registration is ignored.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildWrap all shared plugins with fastify-plugin: const fp = require('fastify-plugin'); module.exports = fp(function (fastify, opts, done) { done(); }).
  • buildEnsure async registration with await: await fastify.register(require('./my-plugin')); or use .ready() callback.
  • buildUse fastify-plugin's encapsulation mode: pass { name: 'plugin-name' } and dependencies option to control ordering.
  • buildRegister shared plugins at the top level (root fastify instance) and use decorate() to expose shared utilities.
  • buildBreak circular dependencies by extracting shared logic into a separate plugin registered first.
  • buildUse fastify.addHook('onRegister', ...) to debug registration order dynamically.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun fastify.printPlugins() after all registrations and confirm the plugin appears in the tree.
  • verifiedAccess the plugin's decorator in a route after registration and verify it's defined.
  • verifiedWrite a unit test that registers the plugin and calls its exposed method.
  • verifiedCheck that the error no longer appears when starting the server with DEBUG=fastify:*.
  • verifiedMonitor the registration order by adding logs before each register() call.
  • verifiedUse fastify.hasDecorator('decoratorName') to programmatically check availability.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't skip the fastify-plugin wrapper for plugins that need to share decorators across scopes.
  • warningDon't register plugins inside conditional blocks or loops that might cause duplicates.
  • warningDon't use fastify.after() to access decorators from a plugin that hasn't been registered yet.
  • warningDon't ignore the order of register() calls — always register dependencies first.
  • warningDon't use module.exports = async function (fastify, opts) — Fastify expects the done callback or returned promise.
  • warningDon't assume plugin names are unique — use explicit names to avoid clashes.
( 07 )War story

The Missing Auth Decorator in Production

Backend LeadNode.js 18, Fastify 4.10, fastify-jwt 6.3, Docker

Timeline

  1. 09:15Deploy v2.3.1 to staging. Server starts fine.
  2. 09:22QA reports all protected routes return 500 with 'decorator not defined'.
  3. 09:30I check logs: 'FastifyError: The decorator 'authenticate' is not defined.'
  4. 09:35I run fastify.printPlugins() — the auth plugin is not in the tree.
  5. 09:40Review the diff: a junior dev moved register(authPlugin) inside a conditional block for 'dev mode'.
  6. 09:45Move the register call to the top level, outside any condition.
  7. 09:48Redeploy to staging — all routes work.
  8. 10:00Root cause: conditional registration skipped the plugin in non-dev environments.

We had just shipped a new feature that required a JWT authentication plugin. Our junior developer, trying to be clever, wrapped the registration of fastify-jwt inside an if (process.env.NODE_ENV === 'development') block so that auth could be skipped during local dev. In staging and production, NODE_ENV was 'staging' and 'production', so the plugin never got registered. The server started fine because no route explicitly checked for the decorator until a request hit a protected route.

I first noticed the error in our error tracking system — it wasn't a crash, just 500s on every protected endpoint. I immediately suspected a registration issue because the error message said 'decorator not defined'. I ran fastify.printPlugins() on the running server (we have a debug endpoint) and didn't see the auth plugin. That confirmed it was never registered.

The fix was simple: remove the condition and always register the plugin. But we also added a test that checks fastify.hasDecorator('authenticate') after registration. We now enforce that all plugins are registered unconditionally and use environment variables only for configuration, not registration logic.

Root cause

Conditional registration of a plugin based on environment variable caused the plugin to be skipped in non-development environments.

The fix

Moved the register() call to the top level, outside any conditional block. Added a deployment-time check to verify all expected decorators are present.

The lesson

Never make registration conditional. Use environment variables for configuration inside the plugin, not for whether the plugin is loaded at all.

( 08 )Understanding Fastify's Plugin Encapsulation

Fastify creates a new child context (encapsulation) for every plugin registered. By default, decorators, hooks, and routes registered inside a plugin are not visible to the parent or sibling plugins. This is intentional to avoid naming collisions and side effects.

To share decorators across plugins, you must either: (1) use the fastify-plugin wrapper to break encapsulation, or (2) register the plugin at a common ancestor scope. The fastify-plugin wrapper explicitly sets the plugin's encapsulation to false, making its decorations visible to all children and siblings at the same level.

( 09 )The Async Registration Trap

Fastify's register() returns a Promise if no callback is provided. If you don't await it, the rest of your code executes before the plugin has finished loading. This leads to 'decorator not defined' errors even though the plugin is eventually registered.

Always use await fastify.register(plugin) inside an async function. If you must use callbacks, pass a function to the third argument: fastify.register(plugin, opts, (err) => { ... }). For top-level code, wrap everything in an async IIFE or use .ready().

( 10 )Debugging with fastify.printPlugins()

Call fastify.printPlugins() at any point after registration to see the plugin tree. It prints a hierarchical view showing each plugin's name, encapsulation status, and dependencies. If your plugin is missing, you know it wasn't registered. If it appears but its decorators are missing, check if it's properly wrapped with fastify-plugin.

You can also call fastify.printPlugins() on the fly by exposing it as a route: fastify.get('/debug/plugins', (req, reply) => { reply.send(fastify.printPlugins()); }). This is invaluable in production to verify registration order without restarting.

( 11 )Dependency Ordering with 'dependencies'

Fastify supports explicit dependency ordering via the 'dependencies' option in fastify-plugin. If plugin B depends on plugin A, you can declare: module.exports = fp(async function (fastify, opts) { ... }, { dependencies: ['pluginA'] }). Fastify will then ensure A is registered before B, or throw an error if A is missing.

This is more reliable than relying on registration order. Use it when you have a chain of plugins. However, be aware that circular dependencies will cause a startup error — Fastify detects them and throws immediately.

( 12 )Common Pitfall: Double Registration in Hot-Reload

If you use a hot-reload tool like nodemon or webpack HMR, the module cache may cause the same plugin to be registered twice. Fastify detects duplicate plugin names and throws 'already registered'. To fix, ensure your plugin has a unique name (pass { name: 'my-plugin' } in options) and clear the require cache on reload.

Alternatively, use the 'childLogger' pattern or dynamic imports with unique names per instance. A simple guard: if (!fastify.hasPlugin('my-plugin')) { fastify.register(plugin); }.

Frequently asked questions

What does 'fastify-plugin: the plugin has likely been already registered' mean?

This error means Fastify detected that the same plugin (by name or reference) is being registered twice. Check for duplicate require() calls or hot-reload causing multiple register() invocations. Each plugin must be registered exactly once.

Can I use a plugin without the fastify-plugin wrapper?

Yes, but without the wrapper, the plugin's decorations are only visible inside that plugin's scope (encapsulated). If you need to share decorators, hooks, or routes with other plugins, you must use the fastify-plugin wrapper to break encapsulation.

Why does my plugin work locally but not in production?

The most common reason is environment-specific code: conditional registration based on NODE_ENV, or different file resolution due to case sensitivity (Linux is case-sensitive). Another cause is missing dependencies in production — ensure your package.json includes all plugins.

How do I check if a plugin is already registered?

Use fastify.hasPlugin('pluginName') to check if a plugin with that name exists. This is useful for guarding against double registration or conditional registration. The plugin name is usually the name passed in options or derived from the function name.

Does registration order matter for plugins that don't share decorators?

Yes, because hooks like onRequest are executed in the order plugins are registered. If plugin A adds a hook and plugin B expects that hook to run before its own, order matters. Use the 'dependencies' option to enforce ordering explicitly.