Skip to content
NestJS ns providers 4 min read

Async Providers

Some dependencies cannot be created synchronously. A database connection must complete a handshake, a secrets manager call must return, a feature-flag service must fetch its initial state. NestJS supports async providers so the application bootstrap process pauses until those resources are genuinely ready, and only then starts accepting requests. This guarantees that no controller or service ever receives a half-initialized dependency.

How async providers work

An async provider is just a useFactory provider whose factory returns a Promise. During app.listen(), Nest resolves the dependency graph; when it encounters a factory that returns a promise, it awaits the result before wiring it into anything that injects it. Because resolution is awaited, every consumer downstream receives the resolved value, not the promise.

The key rule: the HTTP server does not begin listening until all async providers in the graph have settled. If a factory rejects, bootstrap fails fast and the process exits with the error.

import { Module } from '@nestjs/common';

@Module({
  providers: [
    {
      provide: 'ASYNC_CONNECTION',
      useFactory: async () => {
        const connection = await createConnection();
        return connection;
      },
    },
  ],
  exports: ['ASYNC_CONNECTION'],
})
export class DatabaseModule {}

Async providers only delay startup. Nest does not re-await them per request. The factory runs once during bootstrap and the resolved value is cached as a singleton (unless you change its scope).

Awaiting a real database connection

A common use is opening a pooled connection before the app serves traffic. Below, the factory reads config, opens a pool, and verifies it with a ping. Because the factory is async, Nest waits for the await pool.connect() to finish.

// database.providers.ts
import { Pool } from 'pg';
import { ConfigService } from '@nestjs/config';

export const PG_POOL = 'PG_POOL';

export const pgPoolProvider = {
  provide: PG_POOL,
  inject: [ConfigService],
  useFactory: async (config: ConfigService): Promise<Pool> => {
    const pool = new Pool({
      host: config.get<string>('DB_HOST'),
      port: config.get<number>('DB_PORT'),
      user: config.get<string>('DB_USER'),
      password: config.get<string>('DB_PASSWORD'),
      database: config.get<string>('DB_NAME'),
      max: 10,
    });

    // Verify the pool is reachable before bootstrap continues.
    const client = await pool.connect();
    await client.query('SELECT 1');
    client.release();

    return pool;
  },
};
// database.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PG_POOL, pgPoolProvider } from './database.providers';

@Module({
  imports: [ConfigModule],
  providers: [pgPoolProvider],
  exports: [PG_POOL],
})
export class DatabaseModule {}

Inject the resolved pool anywhere with @Inject:

import { Inject, Injectable } from '@nestjs/common';
import { Pool } from 'pg';
import { PG_POOL } from './database.providers';

@Injectable()
export class UsersService {
  constructor(@Inject(PG_POOL) private readonly pool: Pool) {}

  async findById(id: string) {
    const { rows } = await this.pool.query(
      'SELECT id, email FROM users WHERE id = $1',
      [id],
    );
    return rows[0];
  }
}

If you add logging to the factory and start the app, the order of events makes the await visible:

Output:

[Nest] 4821  - LOG [NestFactory] Starting Nest application...
[Nest] 4821  - LOG [InstanceLoader] DatabaseModule dependencies initialized
Connecting to Postgres pool...
Pool ready (ping ok)
[Nest] 4821  - LOG [NestApplication] Nest application successfully started
[Nest] 4821  - LOG Listening on http://localhost:3000

The server line appears after “Pool ready”, proving requests cannot arrive before the connection exists.

Bootstrap when a factory fails

If the resource is unreachable, the rejected promise propagates out of bootstrap(). Handle it so the process exits cleanly rather than hanging.

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

bootstrap().catch((err) => {
  console.error('Bootstrap failed:', err);
  process.exit(1);
});

Output:

Bootstrap failed: error: connect ECONNREFUSED 127.0.0.1:5432

Sync vs async factory

AspectSync useFactoryAsync useFactory
Return typeValuePromise<Value>
Bootstrap impactNoneNest awaits before listening
Use caseCheap, in-memory setupI/O: DB, HTTP, secrets, message brokers
Failure behaviorThrows during resolutionRejection fails bootstrap
Re-run per requestNo (singleton)No (singleton)

Pattern: dynamic module with forRootAsync

Reusable infrastructure modules usually expose a forRootAsync static method so the host app supplies config asynchronously. This is the same async-factory mechanism wrapped in a dynamic-module API.

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Pool } from 'pg';
import { PG_POOL } from './database.providers';

@Module({})
export class DatabaseCoreModule {
  static forRootAsync(): DynamicModule {
    return {
      module: DatabaseCoreModule,
      imports: [ConfigModule],
      providers: [
        {
          provide: PG_POOL,
          inject: [ConfigService],
          useFactory: async (config: ConfigService) => {
            const pool = new Pool({ connectionString: config.get('DATABASE_URL'), max: 10 });
            await pool.query('SELECT 1');
            return pool;
          },
        },
      ],
      exports: [PG_POOL],
    };
  }
}

Always set a sane pool size (max) and graceful shutdown. Pair an async provider that opens a pool with an OnModuleDestroy hook that calls pool.end() so connections close cleanly on SIGTERM.

Best Practices

  • Use async providers only for genuine I/O initialization; keep synchronous setup synchronous to avoid needless bootstrap delay.
  • Verify the resource inside the factory (ping/SELECT 1) so an unreachable dependency fails bootstrap instead of surfacing later as runtime errors.
  • Always inject config through ConfigService rather than reading process.env directly in the factory, keeping providers testable.
  • Wrap bootstrap() in .catch() and process.exit(1) so a failed connection produces a clear non-zero exit for your orchestrator.
  • Implement OnModuleDestroy to close pools and clients, mirroring the async open with an explicit shutdown.
  • Set explicit pool limits (max, timeouts) so a slow startup or leak is bounded and observable.
  • Prefer forRootAsync dynamic modules for shareable infrastructure so each app supplies its own credentials without forking the module.
Last updated June 14, 2026
Was this helpful?