Node runs your code on a single thread, so it does slow things — network calls, database queries, file reads — asynchronously: it starts the work, keeps handling other events, and comes back when the result is ready. Bots and web servers live and die by this. Get it right and one process happily juggles hundreds of Discord events or HTTP requests; get it wrong and a single unhandled promise takes the whole thing down. This guide covers the patterns that fit Falix workloads and the traps to avoid.
| At a glance | |
|---|---|
| You need | A Falix server running the Node.js application |
| Plan | Any |
| Time | Twenty minutes |
| Good to already know | JavaScript basics; a running bot or web app helps |
Promises and async/await
A promise is a value that isn't ready yet. async/await is the readable way to work with promises: await pauses the current function until the promise settles, without blocking the rest of your app.
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
const data = await res.json();
return data;
}
The rule that saves you the most grief: anything that returns a promise must be awaited or have a .catch(). An await you forgot is the root of most async bugs.
Bot-shaped: awaiting inside an event handler
Discord.js fires an event; your handler does async work and replies. Wrap it so one failing command never escapes:
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
try {
const data = await lookupSomething(interaction.options.getString('query'));
await interaction.reply(data);
} catch (err) {
console.error('command failed:', err);
if (!interaction.replied) {
await interaction.reply({ content: 'Something went wrong.', ephemeral: true });
}
}
});
The try/catch around the awaits is what keeps a broken command from crashing the bot — see the next section for exactly why that matters.
Web-shaped: sequential vs parallel
When a request needs several independent things, don't await them one after another — that's as slow as the pieces added together:
// Slow: three round-trips back to back
const user = await getUser(id);
const posts = await getPosts(id);
const stats = await getStats(id);
// Fast: all three at once, wait for the slowest
const [user, posts, stats] = await Promise.all([
getUser(id),
getPosts(id),
getStats(id),
]);
Use sequential await only when a later step needs the earlier result. Everything independent should go through Promise.all.
🎯 Good to know:
Promise.allrejects as soon as any of its promises rejects. If you want every result regardless of individual failures (say, checking several servers where some may be down), usePromise.allSettledand inspect each outcome.
The trap: an unhandled rejection crashes everything
This is the most important thing on the page. When a promise rejects and nothing is there to catch it, modern Node treats it as a fatal error and exits the entire process. On Falix that means your bot goes offline and the server shows as stopped — from one forgotten await in one handler.
// If this rejects and nothing catches it, the WHOLE process exits.
doSomethingAsync(); // no await, no .catch()
The fix is discipline, not a safety net: await every promise inside a try/catch, or attach a .catch() to any promise you deliberately don't await. As a last-resort backstop you can log rejections instead of letting them kill the process silently:
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
});
⚠️ Heads up: Treat that handler as a smoke alarm, not a fix. It stops the instant crash, but a rejection reaching it means real code failed and did nothing about it. Find the un-awaited promise it's logging and handle it properly.
Don't block the event loop
Because everything shares one thread, a long synchronous loop freezes your whole app — the bot stops responding to Discord's heartbeat, the web server stops answering requests, all at once. Anything CPU-heavy (parsing a huge file, hashing in a tight loop, big JSON transforms) should be broken up, moved to an async library, or done in small batches. If your app periodically goes unresponsive with no error in the console, suspect a blocking loop, not the network.
Fire-and-forget, honestly
Sometimes you genuinely don't want to wait — logging, a background ping. That's fine, but still handle the rejection, or one background failure crashes the app:
sendMetric(event).catch((err) => console.error('metric failed:', err));
Troubleshooting
- Bot/app disappears with no obvious error — scroll the console up: an unhandled rejection prints a stack trace and then the process exits. That first trace is the real cause. See My app won't start.
- A command sometimes hangs and never replies — an
awaitthat never settles (a request with no timeout). Add timeouts to outbound calls, and always reply in both thetryand thecatch. - Everything freezes for a few seconds, then resumes — a blocking synchronous operation on the event loop. Break the heavy work into chunks or use an async API.
UnhandledPromiseRejectionin the console — exactly the trap above; find the promise with noawait/.catch().
The promise and event-loop model is standard JavaScript — MDN's asynchronous guide covers the fundamentals in depth.