@typescript-eslint/no-misused-promises

Disallow promises where the code around them cannot handle one

needs types

Also called typescript-eslint/no-misused-promises, typescript/no-misused-promises

A promise is an object, so it's always truthy, and if (isAdmin(user)) without await always takes the branch. An async function passed where a callback should return nothing is never awaited, so forEach moves on before the work is done, and nothing catches a rejection. A promise spread into an object adds no properties.

bun lint --type-aware turns this rule on in TypeScript files.

Configure#

Off by default. This rule needs types, so turning it on makes bun lint type check your code first.

bunfig.toml
[lint.rules]
"@typescript-eslint/no-misused-promises" = "error" # or "warn"

Every option, set to its default:

bunfig.toml
[lint.rules]
"@typescript-eslint/no-misused-promises" = ["error", {
  checksConditionals = true,
  checksVoidReturn = {
    arguments = true,
    attributes = false,
    inheritedMethods = true,
    properties = true,
    returns = true,
    variables = true,
  },
  checksSpreads = true,
}]

Examples#

example.ts3 problems
declare function isAdmin(id: string): Promise<boolean>;
declare function remove(id: string): Promise<void>;
declare function load(id: string): Promise<{ name: string }>;

function cleanUp(ids: string[]) {
  if (isAdmin(ids[0])) {
    ids.forEach(async id => {
      await remove(id);
    });
  }
  return { id: ids[0], ...load(ids[0]) };
}
example.tsNo problems
declare function isAdmin(id: string): Promise<boolean>;
declare function remove(id: string): Promise<void>;
declare function load(id: string): Promise<{ name: string }>;

async function cleanUp(ids: string[]) {
  if (await isAdmin(ids[0])) {
    for (const id of ids) {
      await remove(id);
    }
  }
  return { id: ids[0], ...(await load(ids[0])) };
}

An async callback in filter keeps every item, because the promise it returns is truthy:

example.ts1 problem
declare function isAdmin(id: string): Promise<boolean>;

const admins = ["ada", "lin"].filter(async id => await isAdmin(id));

Options#

OptionDefaultWhat it does
checksConditionalstrueReport a promise as a condition: in if, while, ? :, !, &&, ||, and returned from the callback of filter, find, some and the like.
checksVoidReturn{ "attributes": false }Report a function that returns a promise where a function that returns void is expected.
checksSpreadstrueReport ... in front of a promise.

checksVoidReturn is true, false, or an object that turns the check off in some places. A key you leave out is true.

JSX attributes aren't checked by default, because <form onSubmit={save}> with an async function is how most React code handles events. With an ESLint config file the default is typescript-eslint's, true.

KeyWhere the function is
argumentsAn argument of a call or of new.
attributesA JSX attribute.
inheritedMethodsA method, when the class or interface it extends or implements declares it as returning void.
propertiesA property of an object.
returnsA value that a function returns.
variablesA value assigned to a variable.

Not reported with checksVoidReturn: { arguments: false }:

example.tswith { "checksVoidReturn": { "arguments": false } }No problems
declare function remove(id: string): Promise<void>;

function cleanUp(ids: string[]) {
  ids.forEach(async id => {
    await remove(id);
  });
}

Not reported by default, and reported with checksVoidReturn: true:

example.tsxNo problems
declare namespace JSX {
  interface IntrinsicElements {
    form: { onSubmit?: () => void };
  }
}
declare function save(): Promise<void>;

const form = <form onSubmit={save} />;
example.tsxwith { "checksVoidReturn": true }1 problem
declare namespace JSX {
  interface IntrinsicElements {
    form: { onSubmit?: () => void };
  }
}
declare function save(): Promise<void>;

const form = <form onSubmit={save} />;

checksConditionals also takes { "flagUnions": .. }, for a condition that's only sometimes a promise. With "none" (default) it's allowed. With "all" it's reported. With "strict" it's reported if the union has both the promise and what it resolves to, like number | Promise<number>.

Not reported by default:

example.tsNo problems
declare function cached(key: string): number | Promise<number>;

if (cached("total")) {
  console.log("found");
}

Reported with checksConditionals: { flagUnions: "strict" }:

example.tswith { "checksConditionals": { "flagUnions": "strict" } }1 problem
declare function cached(key: string): number | Promise<number>;

if (cached("total")) {
  console.log("found");
}