This rule raises an issue when the callback passed to describe(), or one of its aliases or variants, is declared async, or when a describe() callback calls an async helper that registers tests after an await.

Why is this an issue?

Test frameworks like Jest, Mocha, and Cypress run a suite’s callback during the discovery phase, before any test executes, so they can collect the test cases nested inside it. The framework expects this callback to be a synchronous function and does not await the promise an async callback returns.

This applies to every suite-defining form whose callback runs during discovery, grouped here by the frameworks that provide it:

Code examples

The callback awaits before registering tests. The framework runs the synchronous portion of the callback, registers whatever was scheduled before the first await, and moves on. Any it() declared after that await is registered too late and is silently skipped. The reported test counts can look fine even though tests the developer intended to run never execute. Move the awaited work into a lifecycle hook the framework awaits, or into the individual test bodies, so that every it() is reached before any await.

Noncompliant code example

describe('user service', async () => { // Noncompliant: an async suite callback is not awaited, so tests after the await are skipped
  const config = await loadConfig();

  it('returns users', () => { // never registered
    expect(listUsers(config)).toEqual(['alice', 'bob']);
  });
});

Compliant solution

describe('user service', () => { // Compliant: the suite callback is synchronous
  let config;

  beforeEach(async () => { // async work moved into a hook the framework awaits
    config = await loadConfig();
  });

  it('returns users', () => {
    expect(listUsers(config)).toEqual(['alice', 'bob']);
  });
});

The callback does not await anything. All tests still register, but the async keyword is misleading and makes the callback return a promise that the framework silently ignores. Remove async to make the callback synchronous.

Noncompliant code example

describe('user service', async () => { // Noncompliant: the suite callback is async but awaits nothing, so async is misleading
  it('returns users', () => {
    expect(listUsers()).toEqual(['alice', 'bob']);
  });
});

Compliant solution

describe('user service', () => { // Compliant: the suite callback is synchronous
  it('returns users', () => {
    expect(listUsers()).toEqual(['alice', 'bob']);
  });
});

A helper called from the describe callback registers tests after an await. The problem is not limited to the describe callback itself. If the describe callback calls an async helper that registers tests after its own await, those tests are silently dropped. Awaiting the helper does not help: the framework does not await the describe() callback. Async work is still valid inside a test callback or lifecycle hook, because those run after discovery instead of defining tests during it.

Noncompliant code example

async function connectAndRegister() {
  const db = await connect();
  it('inserts a record', () => { // never registered: runs after discovery
    db.insert({ id: 1 });
  });
}

describe('database', () => {
  connectAndRegister(); // Noncompliant: async helper registers tests after an await; they are silently dropped
});

Compliant solution

function registerDatabaseTests() {
  it('inserts a record', async () => {
    const db = await connect(); // Compliant: async work inside a test callback runs after discovery
    db.insert({ id: 1 });
  });
}

describe('database', () => {
  registerDatabaseTests();
});

Noncompliant code example

async function loadAndRegister() {
  const config = await loadConfig();
  it('uses the loaded config', () => { // never registered: describe's Promise is ignored by the framework
    expect(getConfig()).toEqual(config);
  });
}

describe('configuration', async () => {
  await loadAndRegister(); // Noncompliant: async helper drops tests even when awaited; the framework does not await the describe callback's Promise
});

Compliant solution

function loadAndRegister() {
  let config;

  beforeEach(async () => { // Compliant: async setup moved into a hook the framework awaits
    config = await loadConfig();
  });

  it('uses the loaded config', () => {
    expect(getConfig()).toEqual(config);
  });
}

describe('configuration', () => {
  loadAndRegister();
});

Exceptions

The rule does not raise on the skip variants, such as describe.skip(), context.skip(), suite.skip(), xdescribe(), describe.skip.each(), and xdescribe.each(). The suite callback still runs during discovery, but because the tests it contains are skipped regardless, an async callback there cannot cause tests that were meant to run to be silently dropped.

Limitations

The rule follows async helper calls one hop deep. An async helper that itself delegates to another async function is not inspected beyond that first call.

The rule only checks helpers defined in the same file as the describe call. Async helpers imported from other modules are not analyzed.

Resources

Documentation