Skip to content

Hot module reloading

During development, restarting your entire bot every time you tweak a command or fix a middleware gets tedious fast. The Discord gateway connection takes a few seconds to re-establish, slash commands need to re-sync, and if you’re debugging a specific interaction flow, you lose your place.

Warden’s hot module reloading (HMR) solves this by replacing individual modules at runtime without a full restart. Edit a command, save the file, and the new version is live in Discord within milliseconds — no reconnect, no re-sync, no lost state.

Add .hmr() to your Bot builder chain:

import {Bot} from '@warden/core';
new Bot()
.discover(d => d
.interactions('./src/{commands,buttons,modals,menus}/**/*.ts')
.events('./src/events/**/*.ts'))
.hmr()
.intents(i => i.defaults())
.partials(p => p.defaults())
.start();

That’s all it takes. Warden starts watching the files discovered by .discover() and reloads them when they change.

When a file changes, Warden performs three steps:

  1. Invalidate — the module is removed from Node’s module cache, so the next import gets the fresh version
  2. Re-import — the file is imported again, and its decorators re-execute to capture any changes
  3. Replace — the new class replaces the old one in the DI container, so the next execution uses the updated version

This all happens in-place. The Discord gateway connection stays alive, event listeners remain attached, and singleton services keep their state. Only the specific handler that changed gets swapped out.

When HMR triggers, the logger outputs a brief message:

[hmr] Reloaded: src/commands/mod/ModWarnCommand.ts

If the reload fails (for example, a syntax error in your file), Warden logs the error and keeps the previous version running:

[hmr] Failed to reload: src/commands/mod/ModWarnCommand.ts
SyntaxError: Unexpected token at line 42
[hmr] Keeping previous version

Your bot keeps working — you just fix the error and save again.

Everything that .discover() finds is eligible for HMR. This covers the full range of decorated classes:

TypeDecoratorHMR supported
Commands@command()Yes
Events@event()Yes
Buttons@button()Yes
Modals@modal()Yes
Select menus@menu()Yes
Jobs@job()Yes
MiddlewareMiddleware (extends)Yes
Error handlers@errorHandler()Yes
Config filessrc/config/*.tsYes
Decomposers@decompose()Yes

When a config file changes, the Config service picks up the new values immediately. When middleware changes, the next execution uses the updated version. The same applies across the board.

You might be wondering how HMR relates to tsx watch, which the scaffolder configures for pnpm dev. They serve complementary purposes:

tsx watchWarden HMR
MechanismKills and restarts the entire Node.js processReplaces individual modules in the running process
SpeedA few seconds (gateway reconnect, command sync)Milliseconds (no reconnect needed)
ScopeEverything restarts freshOnly the changed file is reloaded
Singleton stateLost on restartPreserved
Structural changesHandles everything (new files, deleted files, package.json changes)Handles changes to existing files

In practice, they work together beautifully. HMR handles the fast-iteration case — you’re editing a command’s execute method, tweaking a middleware condition, adjusting a config value. When you make structural changes — adding entirely new files, installing packages, changing the bootstrap — tsx watch catches those and does a full restart.

In production, .hmr() is a no-op. It doesn’t register file watchers, doesn’t watch for changes, and adds zero overhead. You can safely leave it in your bootstrap file without any conditional logic:

// This is perfectly fine for production
new Bot()
.discover(d => d
.interactions('./dist/{commands,buttons,modals,menus}/**/*.js')
.events('./dist/events/**/*.js'))
.hmr() // does nothing in production
.intents(i => i.defaults())
.partials(p => p.defaults())
.start();

Warden detects the environment automatically. When NODE_ENV is set to production, or when the scanned files are compiled JavaScript (.js), HMR is silently skipped. No configuration needed.