Hot module reloading
Introduction
Section titled “Introduction”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.
Enabling HMR
Section titled “Enabling HMR”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.
How it works
Section titled “How it works”When a file changes, Warden performs three steps:
- Invalidate — the module is removed from Node’s module cache, so the next import gets the fresh version
- Re-import — the file is imported again, and its decorators re-execute to capture any changes
- 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.
What you’ll see
Section titled “What you’ll see”When HMR triggers, the logger outputs a brief message:
[hmr] Reloaded: src/commands/mod/ModWarnCommand.tsIf 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 versionYour bot keeps working — you just fix the error and save again.
What gets reloaded
Section titled “What gets reloaded”Everything that .discover() finds is eligible for HMR. This covers the full range of decorated classes:
| Type | Decorator | HMR supported |
|---|---|---|
| Commands | @command() | Yes |
| Events | @event() | Yes |
| Buttons | @button() | Yes |
| Modals | @modal() | Yes |
| Select menus | @menu() | Yes |
| Jobs | @job() | Yes |
| Middleware | Middleware (extends) | Yes |
| Error handlers | @errorHandler() | Yes |
| Config files | src/config/*.ts | Yes |
| 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.
HMR vs tsx watch
Section titled “HMR vs tsx watch”You might be wondering how HMR relates to tsx watch, which the scaffolder configures for pnpm dev. They serve complementary purposes:
| tsx watch | Warden HMR | |
|---|---|---|
| Mechanism | Kills and restarts the entire Node.js process | Replaces individual modules in the running process |
| Speed | A few seconds (gateway reconnect, command sync) | Milliseconds (no reconnect needed) |
| Scope | Everything restarts fresh | Only the changed file is reloaded |
| Singleton state | Lost on restart | Preserved |
| Structural changes | Handles 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.
Production behavior
Section titled “Production behavior”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 productionnew 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.