Async patterns for bots and web apps

Promises, async/await, and the one mistake that silently crashes your whole app. The async patterns that fit Discord bots and web servers on Falix, with the traps called out.

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.all rejects 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), use Promise.allSettled and 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 await that never settles (a request with no timeout). Add timeouts to outbound calls, and always reply in both the try and the catch.
  • 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.
  • UnhandledPromiseRejection in the console — exactly the trap above; find the promise with no await/.catch().

The promise and event-loop model is standard JavaScript — MDN's asynchronous guide covers the fundamentals in depth.


Next steps

Was this guide helpful?