JCC Express

Digging Deeper

Deferred Functions

Introduction

defer lets you run a callback after the HTTP response has been sent to the client. This is useful for work that the client doesn't need to wait for — sending emails, writing logs, triggering notifications, syncing data to a third-party service, etc.

It works by attaching the callback to the finish event of the current response object, so the response is delivered immediately and the deferred work runs in the background on the same Node/Bun process.


Basic usage

defer is a global helper available anywhere in your app code without any import.

TypeScript
defer(async () => {
  // runs after the response is sent
  await Mail.to(user.email).send({
    content: "views.email.welcome",
    subject: "Welcome!",
    sender: "App",
  });
});

return res.json({ message: "Registered successfully" });

The client receives { message: "Registered successfully" } immediately. The email is sent in the background afterwards.


Inside a FormRequest

A common pattern is calling defer inside a FormRequest save() method, keeping side-effects out of the controller:

TypeScript
// app/Http/Requests/UserRequest.ts
export class UserRequest extends FormRequest {
  protected async rules() {
    return await this.validate({
      name: ["required"],
      email: ["required", "email", "unique:user"],
      password: ["required", "min:6"],
    });
  }

  public async save() {
    const user = await User.create({
      name: this.input("name"),
      email: this.input("email"),
      password: await bcrypt(this.input("password")),
    });

    defer(async () => {
      await Mail.to(user.email).send({
        content: "views.email.welcome",
        subject: "Welcome to the app",
        sender: "Support",
      });
    });

    return user;
  }
}

The controller stays clean — it just calls request.save() and returns:

TypeScript
// UsersController.ts
async store(request: UserRequest, res: AppResponse) {
  const user = await request.save();
  return res.json({ user });
}

Inside a controller

You can call defer directly inside a controller action when you don't need a FormRequest:

TypeScript
async store(req: AppRequest, res: AppResponse) {
  const user = await User.create(req.body);

  defer(async () => {
    await dispatch(new SendWelcomeEmailJob(user.id));
  });

  return res.json({ user });
}

Error handling

If the deferred callback throws, the error is caught and logged to the console. It does not propagate to the client — the response was already sent.

Deferred task failed: Error: SMTP connection refused

Make sure to handle retries or dead-letter queuing inside the callback if reliability matters. For durable background work, use the [Queue](../Digging Deeper/Queues.md) system instead.


How it works

defer() registers a callback on the current response's finish event. The callback runs after the response is fully sent to the client. response() returns the active Express response from the per-request container binding.


When to use defer vs queues

ScenarioUse
Fire-and-forget within the same processdefer
Work that must survive a process restartQueue
Work that needs retries on failureQueue
Sending a welcome email after registrationEither — defer is simpler
Processing uploaded filesQueue
Sending a webhook to a slow third-party APIQueue

defer is the lightweight option — zero setup, runs in the same process. Queues add durability and retry logic at the cost of a worker process and a queue driver.