Async Middleware

Await Your DB Calls Inside Middleware — Never Let a Promise Float

Async Middleware

Koa middleware is async by design, making it natural to await database queries, HTTP calls, or file reads. This lesson covers correct patterns and the silent bug introduced by a missing await.

3 min read Level 2/5 #koa#middleware#async
What you'll learn
  • Await async operations (DB, fetch) safely inside middleware
  • Understand why await next() is required, not optional
  • Identify and fix the floating-promise bug

Because Koa uses async/await end-to-end, you can freely await anything inside middleware — database queries, fetch calls, file reads — without callback nesting.

Awaiting a Database Query

import Koa from 'koa';
import { db } from './db.js';

const app = new Koa();

// Attach the current user to ctx.state for downstream middleware
app.use(async (ctx, next) => {
  const token = ctx.cookies.get('session');
  if (token) {
    ctx.state.user = await db.sessions.findUser(token); // awaited safely
  }
  await next();
});

app.use(async (ctx) => {
  ctx.body = { user: ctx.state.user ?? null };
});

app.listen(3000);

Awaiting fetch

app.use(async (ctx, next) => {
  const res = await fetch('https://api.example.com/flags');
  ctx.state.flags = await res.json();
  await next();
});

The Floating-Promise Bug

This is the most common Koa mistake. Omitting await before next() lets the pipeline continue without waiting for inner layers to complete.

// WRONG
app.use(async (ctx, next) => {
  next();               // returns a Promise nobody awaits
  ctx.set('X-Time', String(Date.now())); // runs BEFORE inner handlers finish
});

// RIGHT
app.use(async (ctx, next) => {
  await next();         // waits for all inner layers
  ctx.set('X-Time', String(Date.now())); // correct: downstream phase
});

The bug is especially subtle because the response often still reaches the client — but timing headers, logging, and error handling all break silently.

return next() vs await next()

Both are valid in Koa when the middleware does nothing after next(). return next() propagates the returned Promise upward; await next() pauses execution. For clarity, prefer await next() unless you are at the last statement.

// Equivalent when next() is the last action
app.use((ctx, next) => return next());      // passes Promise up
app.use(async (ctx, next) => { await next(); }); // awaits it here

Parallel Async Work Before next()

If multiple upstream tasks are independent, run them in parallel with Promise.all before calling next().

app.use(async (ctx, next) => {
  const [flags, config] = await Promise.all([
    fetchFeatureFlags(),
    fetchRemoteConfig(),
  ]);
  ctx.state.flags = flags;
  ctx.state.config = config;
  await next();
});

Up Next

The order in which you register middleware determines exactly what happens — the next lesson explains how to place each type correctly.

Middleware Order Matters →