This rule raises an issue when an object, class, or module defines a property or method named then.

Why is this an issue?

JavaScript treats objects with a then property as "thenable" - similar to Promises. This can cause unexpected behavior when these objects are used with await expressions or dynamic imports.

When JavaScript encounters an object with a then property in an await expression, it assumes the object is Promise-like and tries to resolve it. This can lead to:

The same issue occurs with dynamic imports. If a module exports a then function, the import statement will treat the entire module as thenable, potentially calling the then function instead of returning the module object.

This behavior stems from JavaScript’s Promise resolution algorithm, which checks for a then property to determine if an object should be treated as a Promise.

What is the potential impact?

This issue can cause applications to hang indefinitely, throw unexpected errors, or fail silently. In production environments, this could lead to unresponsive user interfaces, incomplete data processing, or application crashes. The debugging experience is particularly poor because the connection between the then property and the problematic behavior is not immediately obvious.

How to fix it

Rename the then property to a more descriptive name that reflects its actual purpose.

Code examples

Noncompliant code example

const userAction = {
  userId: 123,
  then() { // Noncompliant
    console.log('Action completed');
  }
};

Compliant solution

const userAction = {
  userId: 123,
  complete() {
    console.log('Action completed');
  }
};

Exceptions

The rule does not raise an issue when then is used as part of a JSON Schema conditional validation construct alongside an if property. In JSON Schema, if/then/else are standard keywords for conditional schema application and do not create thenable behavior.

const schema = {
  if: { properties: { age: { minimum: 18 } } },
  then: { required: ['driversLicense'] }, // Compliant - JSON Schema conditional
  else: { required: ['parentConsent'] },
};

No issue will be raised when then is used as a type reference (assigning the Function constructor as a value) in an interface shape descriptor. This pattern describes the expected interface shape of an object, not a method creating thenable behavior.

const connectionInterface = {
  open: Function,
  send: Function,
  then: Function, // Compliant - interface shape descriptor
  close: Function,
};

No issue will be raised when a then property is inside an object that also has an is property and the project uses a validation library such as Yup or Joi. This {is, then} pattern is recognized as a conditional validation configuration used by methods like .when() and .conditional() in these libraries.

import * as yup from 'yup';

const schema = yup.string().when('hasName', {
  is: true,
  then: (schema) => schema.required(), // Compliant: conditional validation config
});

const formSchema = yup.object().shape({
  parentalConsent: yup.bool().when('age', {
    is: (age) => age < 18,
    then: (schema) => schema.required(), // Compliant: conditional validation config
  }),
});
import Joi from 'joi';

const schema = Joi.string().when('type', {
  is: 'email',
  then: Joi.string().email().required(), // Compliant: conditional validation config
});

No issue will be raised on a class’s non-static then member (method or field) when that same class declares an explicit thenable contract, either via a JSDoc @implements tag or a TypeScript implements clause naming IThenable, Thenable, or PromiseLike (with or without type arguments). This documents an intentional Promise-like implementation rather than an accidental thenable.

/**
 * @implements {IThenable<string>}
 */
class Task {
  then(onFulfilled) { // Compliant: class declares an explicit thenable contract
    return onFulfilled('done');
  }
}
class Task implements PromiseLike<string> {
  private readonly promise = Promise.resolve('done');

  then<TResult1 = string, TResult2 = never>( // Compliant: class declares an explicit thenable contract
    onFulfilled?: ((value: string) => TResult1 | PromiseLike<TResult1>) | null,
    onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
  ): PromiseLike<TResult1 | TResult2> {
    return this.promise.then(onFulfilled, onRejected);
  }
}

This exception applies to the nearest enclosing class only, and does not extend to static then members: implements/@implements describes the instance shape, not the class object itself, which can still be treated as thenable (e.g. by a dynamic import).

The contract is recognized by name: the rule takes the declaration at face value and does not check that the named type is genuinely Promise-compatible. Only annotate a class this way when its then really does honor Promise semantics.

interface Thenable {
  then(onFulfilled: (value: string) => void): void;
}

class Row implements Thenable {
  private readonly pending: ((value: string) => void)[] = [];

  then(onFulfilled: (value: string) => void) { // No issue raised, but `await row` never settles
    this.pending.push(onFulfilled); // queued, never invoked
  }
}

If none of the cases above apply and the rule still flags a then you believe is safe, look again at how that object is used: if it can ever reach an await expression or a dynamic import(), keeping the name then is risky even if today’s code happens to work, and renaming it is the safer choice. Only treat the finding as a false positive, and leave the code as-is, when you can rule out both await and dynamic import() ever seeing that object.

Resources

Documentation