R11BREAKINGfunction

Sync to Async

Flags when a synchronous function becomes async. The return type changes from T to Promise<T>, breaking all callers that expect a synchronous return value.


Applies to

TypeScriptPythonGoJavaRust

Why it matters

Adding the async keyword changes the function's return type from T to Promise. Every caller that uses the return value directly (const x = fn()) will now get a Promise object instead of the actual value. This is a silent logical bug if the caller doesn't await the result.

Example

Before (sync)
// validator.ts
export function validate(input: string): boolean {
  return schema.test(input);
}

// caller.ts
if (validate(userInput)) {
  proceed();
}
After (async)
// validator.ts (BREAKING)
export async function validate(input: string): Promise<boolean> {
  const schema = await loadSchema();
  return schema.test(input);
}

// caller.ts — NOW A BUG!
// validate() returns Promise<boolean>, which is always truthy
if (validate(userInput)) {
  proceed(); // ALWAYS runs — Promise object is truthy
}

What you see in the terminal

$ npx dg check

  [BREAKING] validate (signature_change)
  src/validator.ts:2
  Function changed from sync to async. Return type is now Promise<boolean> instead of boolean. Callers using the return value without await will receive a Promise object.

How detection works

The classifier checks the async boolean on both signatures. If the old signature was not async and the new one is, the rule fires.

Real-world scenario

A validation function is changed from sync to async because it now needs to load a schema from a database. The `if (validate(input))` check in 15 call sites now always evaluates to `true` because a Promise object (even one that resolves to false) is truthy in JavaScript.

Edge cases

  • If the function already returned Promise without the async keyword, this rule may not fire
  • Python's def to async def is caught by this rule
  • In Go, adding a channel return is conceptually similar but not detected by this rule