Async Misuse Anti-Patterns — Exercises¶
Category: Async Anti-Patterns → Misuse — hands-on practice removing async machinery applied where it doesn't help. Covers (collectively): Promise Constructor Anti-Pattern ·
asyncWithoutawait
These are fix-it exercises, not recognition quizzes. Each one gives you a problem statement, a starting snippet (mostly JavaScript/TypeScript, some Python asyncio — the language varies on purpose), acceptance criteria with hints, and a collapsible solution. The job is to make the change: unwrap a needless new Promise, plug an error leak, drop a pointless async, and — the part that separates copy-paste from understanding — recognize the legitimate cases where new Promise and a no-await async are exactly right.
How to use this file. Read the problem, write the fix in your editor before opening the solution, then compare. The "why it's better" note under each solution carries the real lesson: in async code the difference between two snippets is rarely lines saved — it is whether an error can still reach a
catch. Refer back tojunior.mdfor the shapes andmiddle.mdfor the cures.The two anti-patterns in one sentence each. The Promise Constructor anti-pattern (a.k.a. the explicit-construction or deferred anti-pattern) is wrapping something that is already a Promise in
new Promise(...), which adds a layer that silently drops errors.asyncwithoutawaitis marking a functionasyncwhen its body never suspends, paying a microtask hop and an extraPromisewrapper for nothing. Both are "async theater": ceremony that looks asynchronous but buys you no concurrency and often loses error fidelity.
Table of Contents¶
| # | Exercise | Anti-pattern(s) | Lang | Difficulty |
|---|---|---|---|---|
| 1 | Unwrap the needless new Promise | Promise Constructor | JS | ★ easy |
| 2 | Drop the pointless async | async w/o await | TS | ★ easy |
| 3 | Plug the error leak | Promise Constructor | JS | ★ easy |
| 4 | Stop double-wrapping fetch | Promise Constructor | JS | ★ easy |
| 5 | The legitimate new Promise — bridge setTimeout | (correct use) | JS | ★★ medium |
| 6 | Bridge a Node-style callback — promisify by hand | (correct use) | JS | ★★ medium |
| 7 | Fix the async Promise executor | Promise Constructor + async | JS | ★★ medium |
| 8 | Keep the async — normalize a sometimes-sync API | async w/o await (justified) | TS | ★★ medium |
| 9 | Bridge a one-shot event with cleanup | (correct use) | JS | ★★ medium |
| 10 | Write a timeout wrapper | (correct use) | TS | ★★★ hard |
| 11 | Add cancellation with AbortSignal | (correct use) | TS | ★★★ hard |
| 12 | Python: stop wrapping a coroutine in a Future | Promise Constructor (asyncio) | Python | ★★ medium |
| 13 | Python: the no-await async def | async w/o await | Python | ★★ medium |
| 14 | Mini-project: clean up the Downloader | both | TS | ★★★★ project |
| 15 | Write a misuse review checklist + lint config | meta | — | ★★ medium |
Exercise 1 — Unwrap the needless new Promise¶
Anti-pattern: Promise Constructor · Language: JavaScript · Difficulty: ★ easy
getUser already returns a Promise. Someone wrapped it in another. Unwrap it.
function getUser(id) {
return new Promise((resolve) => {
db.query("SELECT * FROM users WHERE id = ?", [id]).then((rows) => {
resolve(rows[0]);
});
});
}
Acceptance criteria - No new Promise remains. - The function still resolves to rows[0]. - A rejection from db.query now propagates to the caller's .catch (the original silently lost it).
Hint: db.query(...).then(...) is already a Promise. Return it; transform the value in the .then.
Solution
Or, more readably, with `async`/`await` (here the `await` is real, so `async` is justified): **Why it's better.** The original built a brand-new Promise whose *only* job was to mirror the one `db.query` already returns — pure overhead. Worse, the executor only called `resolve`; it never registered a rejection handler, so **if `db.query` rejected, that rejection vanished**: the outer Promise would hang forever (it is never resolved or rejected), and you would see an unhandled-rejection warning with a stack trace pointing nowhere useful. Returning the inner Promise (or `await`ing it) makes errors flow to the caller's `.catch`/`try` automatically, because `.then` propagates rejection by default. The rule: *never wrap a Promise in `new Promise` — you already have one.*Exercise 2 — Drop the pointless async¶
Anti-pattern: async without await · Language: TypeScript · Difficulty: ★ easy
This function is marked async but never awaits anything. Decide whether the async earns its keep, and if not, remove it.
async function fullName(user: { first: string; last: string }): Promise<string> {
return `${user.first} ${user.last}`;
}
Acceptance criteria - The function's body does only synchronous string work, so the async is removed. - The return type reflects the synchronous nature. - Callers that did await fullName(u) still compile (because await on a plain value is legal).
Hint: await on a non-Promise is a no-op that still costs a microtask tick. A function with no real await should not be async.
Solution
function fullName(user: { first: string; last: string }): string {
return `${user.first} ${user.last}`;
}
Exercise 3 — Plug the error leak¶
Anti-pattern: Promise Constructor (lost rejection) · Language: JavaScript · Difficulty: ★ easy
This wrapper does handle errors — into a black hole. A failed loadConfig leaves the returned Promise pending forever. Fix it two ways: the quick unwrap, and (for the sake of the drill) the minimal correct executor.
function getConfig() {
return new Promise((resolve) => {
loadConfig()
.then((cfg) => resolve(cfg))
.catch((err) => {
console.error("config failed", err); // logged, then swallowed
// no reject(err), no resolve — the outer Promise hangs
});
});
}
Acceptance criteria - A failure in loadConfig rejects the returned Promise (it must not hang). - The preferred fix removes new Promise entirely. - If you keep the executor (only as an exercise), it must call reject on failure.
Hint: the bug is a Promise that is neither resolved nor rejected on the error path — the worst failure mode, because it produces no signal at all, just a hung await.
Solution
**Preferred — unwrap it.** The wrapper adds nothing; return the inner Promise and let rejection flow:function getConfig() {
return loadConfig(); // errors propagate to the caller's .catch automatically
}
Exercise 4 — Stop double-wrapping fetch¶
Anti-pattern: Promise Constructor · Language: JavaScript · Difficulty: ★ easy
A helper wraps fetch to return JSON. It rebuilds the Promise machinery fetch already provides, and loses network errors in the process.
function getJSON(url) {
return new Promise((resolve, reject) => {
fetch(url).then((res) => {
res.json().then((data) => resolve(data));
});
// fetch can reject (network down) — nothing catches it here
});
}
Acceptance criteria - No new Promise. - A network failure (fetch rejects) and a malformed-body failure (res.json() rejects) both propagate. - A non-2xx HTTP status is surfaced as an error (since fetch does not reject on 4xx/5xx).
Hint: chain the two Promises and return the chain. Add an explicit res.ok check, because fetch resolves even on a 404.
Solution
Equivalent without `async` (the `.then` chain is itself a Promise — no `new Promise` needed): **Why it's better.** The original `new Promise` wrapper registered no rejection handler on either inner Promise, so a dropped connection or a body that is not valid JSON would leave the outer Promise pending forever. The chained version lets both failures propagate naturally. It also fixes a *semantic* bug the wrapper hid: `fetch` only rejects on network-level failure, **not** on a 4xx/5xx response — so without the `res.ok` guard, a 500 error page would be parsed as "successful" JSON (or throw a confusing parse error). The rewrite makes "this is an error" explicit. Here `async` is justified because there is a real `await`; the non-`async` form is equally correct.Exercise 5 — The legitimate new Promise — bridge setTimeout¶
Anti-pattern: (none — this is the correct use of new Promise) · Language: JavaScript · Difficulty: ★★ medium
new Promise is the wrong tool when you already have a Promise — but it is the right tool to bridge a callback / timer / event API that predates Promises. Write a delay(ms) that resolves after ms milliseconds, supporting optional cancellation.
// Goal: const t = delay(1000); await t; // resolves after 1s
// Stretch: delay should be cancelable so a hung test can't wait forever.
function delay(ms) {
// TODO: bridge setTimeout, which is callback-based, into a Promise
}
Acceptance criteria - Uses new Promise legitimately (there is no existing Promise to return — setTimeout is callback-based). - Resolves exactly once after ms. - Bonus: returns a way to cancel the pending timer (and the canceled Promise neither resolves nor leaks the timer).
Hint: this is the canonical legitimate new Promise. The executor wraps setTimeout and calls resolve from the callback. For cancellation, clearTimeout and reject (or resolve) deliberately.
Solution
With cancellation, returning both the Promise and a `cancel` function, cleaning up the timer:function delay(ms) {
let timer;
const promise = new Promise((resolve, reject) => {
timer = setTimeout(resolve, ms);
// expose a cancel that clears the timer AND settles the Promise
promise.cancel = () => {
clearTimeout(timer);
reject(new Error("delay canceled"));
};
});
return promise;
}
// usage:
// const d = delay(1000);
// d.cancel(); // clears the timer, rejects with "delay canceled"
// await d; // throws if canceled
Exercise 6 — Bridge a Node-style callback — promisify by hand¶
Anti-pattern: (none — correct use; also relates to "Mixing Callbacks and Promises") · Language: JavaScript · Difficulty: ★★ medium
You have an old Node-style API: a function whose last argument is a (err, result) callback. Promisify it correctly by hand (then note the standard-library shortcut). This is a legitimate new Promise.
// fs.readFile(path, encoding, (err, data) => ...) — error-first callback.
// Goal: const text = await readFileP("a.txt", "utf8");
function readFileP(path, encoding) {
// TODO: wrap fs.readFile so err -> reject, data -> resolve
}
Acceptance criteria - err (the first callback argument) rejects the Promise. - data (the second) resolves it. - The callback fires exactly once; the Promise settles exactly once. - Note the production shortcut.
Hint: error-first convention: (err, data) => err ? reject(err) : resolve(data). In real code, prefer util.promisify or fs.promises.
Solution
const fs = require("node:fs");
function readFileP(path, encoding) {
return new Promise((resolve, reject) => {
fs.readFile(path, encoding, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
Exercise 7 — Fix the async Promise executor¶
Anti-pattern: Promise Constructor + async executor · Language: JavaScript · Difficulty: ★★ medium
This code passes an async function as the Promise executor. That is a known footgun: errors thrown inside the async executor become unhandled rejections of the executor's own Promise, not the one being constructed — so reject never fires.
function loadAll(ids) {
return new Promise(async (resolve, reject) => {
const results = [];
for (const id of ids) {
const item = await fetchItem(id); // if this rejects, it does NOT reject the outer Promise
results.push(item);
}
resolve(results);
});
}
Acceptance criteria - No async executor passed to new Promise. - A rejection from any fetchItem(id) rejects the returned Promise. - Prefer the form with no new Promise at all.
Hint: if you have await, you don't need new Promise — just write an async function. The whole construct collapses.
Solution
If the items are independent, fetch them in parallel instead of serially: **Why it's better.** The `new Promise(async ...)` pattern is doubly wrong. First, it is redundant: an `async` function *already* returns a Promise, so wrapping it in `new Promise` is the constructor anti-pattern. Second, it is a **silent error leak**: when the `async` executor throws (e.g. `fetchItem` rejects), that error rejects the *invisible* Promise that the `async` executor itself returns — which nobody holds a reference to — so it becomes an unhandled rejection while the outer Promise stays **pending forever**. `reject` is never reached. Dropping `new Promise` and writing a plain `async` function fixes both: a throw inside an `async` function rejects *that function's* returned Promise, which is exactly the one the caller awaits. The `Promise.all` variant additionally fixes an [`await`-in-a-loop](../README.md) serialization that the original buried. Most linters flag `no-async-promise-executor` for this exact reason.Exercise 8 — Keep the async — normalize a sometimes-sync API¶
Anti-pattern: async without await (justified — do not remove it here) · Language: TypeScript · Difficulty: ★★ medium
Not every no-await async is wrong. A cache lookup returns synchronously on a hit and asynchronously on a miss. The async keyword normalizes both into a Promise so callers have one shape. Decide what to do — and justify keeping async.
const cache = new Map<string, User>();
// Looks like an "async without await" smell on the hit path... is it?
async function getUser(id: string): Promise<User> {
const cached = cache.get(id);
if (cached) {
return cached; // synchronous hit — no await
}
const user = await fetchUser(id); // async miss — real await
cache.set(id, user);
return user;
}
Acceptance criteria - Recognize that this function does contain a real await (the miss path), so it is not the anti-pattern. - Keep the async and explain why a "uniform Promise return" is the goal. - As a contrast, show the bad alternative (returning a raw value on hit, a Promise on miss) and why it breaks callers.
Solution
**Keep it exactly as written.** It already contains a real `await` on the miss path, so it is not "`async` without `await`." Even on the synchronous hit path, `async` is doing useful work: it guarantees a **single return type** — `Promise// BAD: union return type — callers can't tell whether to await.
function getUserBad(id: string): User | Promise<User> {
const cached = cache.get(id);
if (cached) return cached; // a User
return fetchUser(id).then((u) => { // a Promise<User>
cache.set(id, u);
return u;
});
}
// Caller can't write uniform code:
// const u = getUserBad(id); // is u a User or a Promise<User>? depends on cache!
// u.name; // works on hit, fails on miss
Exercise 9 — Bridge a one-shot event with cleanup¶
Anti-pattern: (none — correct use of new Promise) · Language: JavaScript · Difficulty: ★★ medium
Turn a one-shot event into a Promise. The trap: if you only register the success listener, an error event leaves the Promise pending forever, and listeners that never fire leak memory. Wire both, and clean up.
// An EventEmitter that emits "open" once on success, "error" once on failure.
function waitForOpen(socket) {
return new Promise((resolve) => {
socket.on("open", resolve); // only half the story — and never removed
});
}
Acceptance criteria - Resolve on "open", reject on "error". - Use once (or remove listeners) so neither listener leaks after the Promise settles. - Removing the other listener on settle prevents both a leak and a late double-settle.
Hint: register both listeners; on whichever fires first, remove both before settling. Node's once/removeListener (or events.once) handle this.
Solution
function waitForOpen(socket) {
return new Promise((resolve, reject) => {
const onOpen = () => {
cleanup();
resolve();
};
const onError = (err) => {
cleanup();
reject(err);
};
const cleanup = () => {
socket.removeListener("open", onOpen);
socket.removeListener("error", onError);
};
socket.once("open", onOpen);
socket.once("error", onError);
});
}
Exercise 10 — Write a timeout wrapper¶
Anti-pattern: (none — correct use of new Promise; composing with Promise.race) · Language: TypeScript · Difficulty: ★★★ hard
Wrap any Promise so it rejects if it does not settle within ms. The subtle parts: clear the timer when the work wins (or you leak it), and make the timeout reject (not resolve) so callers can distinguish.
// Goal: await withTimeout(fetchUser(id), 2000) -> rejects "timeout" if fetch is too slow.
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
// TODO
}
Acceptance criteria - Rejects with a clear timeout error if work does not settle within ms. - If work settles first, the timer is cleared (no dangling setTimeout). - The original work Promise is returned/raced, not re-wrapped needlessly.
Hint: Promise.race([work, timeout]). The timeout is a legitimate new Promise over setTimeout. Use finally to clearTimeout regardless of which side wins.
Solution
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
let timer: ReturnType<typeof setTimeout>;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
});
// Race the real work against the timeout; clear the timer whichever wins.
return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
}
Exercise 11 — Add cancellation with AbortSignal¶
Anti-pattern: (none — correct use; the modern alternative to a hand-rolled cancel) · Language: TypeScript · Difficulty: ★★★ hard
withTimeout (Exercise 10) leaves the work running. The modern, composable answer is AbortController/AbortSignal, which fetch and many APIs accept natively. Build a fetchWithTimeout that actually aborts the request on timeout, with no leaked timer.
// Goal: a fetch that aborts the underlying request after `ms` and cleans up.
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
// TODO: use AbortController so the request itself is canceled, not just abandoned
}
Acceptance criteria - On timeout, the underlying fetch is aborted (the request stops), not merely ignored. - The timer is cleared if the fetch completes first. - The thrown error distinguishes a timeout abort from other failures.
Hint: AbortController + controller.abort() from a setTimeout. Pass controller.signal to fetch. Clear the timer in finally. AbortSignal.timeout(ms) is the one-liner standard-library version.
Solution
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} catch (err) {
if (controller.signal.aborted) {
throw new Error(`fetch to ${url} timed out after ${ms}ms`);
}
throw err; // a non-abort failure (network, DNS) propagates unchanged
} finally {
clearTimeout(timer); // cleared whether we succeeded, timed out, or errored
}
}
Exercise 12 — Python: stop wrapping a coroutine in a Future¶
Anti-pattern: Promise Constructor (asyncio equivalent) · Language: Python asyncio · Difficulty: ★★ medium
The Python analogue of new Promise(r => existing.then(r)) is manually creating a Future, scheduling a task to copy the coroutine's result into it, and returning the Future. It is pure overhead and drops exceptions. Unwrap it.
import asyncio
def get_user(user_id):
loop = asyncio.get_event_loop()
fut = loop.create_future()
async def runner():
rows = await db.query("SELECT * FROM users WHERE id = $1", user_id)
fut.set_result(rows[0]) # if db.query raises, the exception is never set on fut
asyncio.ensure_future(runner())
return fut
Acceptance criteria - No manual create_future / set_result. - get_user becomes an ordinary coroutine awaited by callers. - An exception from db.query propagates to the awaiter (the original lost it).
Hint: you already have a coroutine — just await it. The hand-rolled Future is asyncio's version of the Promise-constructor anti-pattern.
Solution
**Why it's better.** The original is the `asyncio` mirror of Exercise 1: a coroutine's result is *already* awaitable, so creating a separate `Future` and copying the value across is redundant machinery. And it leaks errors exactly like the JS version — `runner` only calls `fut.set_result(...)` on success; if `db.query` raises, the exception propagates out of the orphaned `runner` task (surfacing as a *"Task exception was never retrieved"* warning), while `fut` is **never resolved**, so `await get_user(id)` hangs forever. Writing a plain `async def` and `await`ing the query lets the exception flow to the caller's `try/except` naturally. The hand-rolled `Future` (and the bare `ensure_future` that orphans the task) is a code smell in `asyncio`: you almost never need `create_future` outside of bridging a genuinely callback-based, non-coroutine API (e.g. `loop.call_soon`, a protocol callback) — the direct analogue of the legitimate `new Promise` cases above.Exercise 13 — Python: the no-await async def¶
Anti-pattern: async without await · Language: Python asyncio · Difficulty: ★★ medium
This function is declared async def but never awaits. Callers must await it for no reason, and forgetting the await is a silent bug. Decide whether to keep async def.
Acceptance criteria - The body is purely synchronous, so async def is removed. - Callers call it directly (no await). - Explain the specific Python hazard of a no-await async def that JS does not have.
Solution
**Why it's better.** As in Exercise 2, `async` here buys nothing — there is no I/O to await. But Python's hazard is sharper than JavaScript's. Calling an `async def` does **not run it**: it returns a *coroutine object*. So `slug = slugify(title)` (forgetting `await`) does not raise — `slug` is a coroutine, the string transformation never runs, and you get a bug like `TypeError: sequence item 0: expected str instance, coroutine found` far downstream, or a *"coroutine was never awaited"* `RuntimeWarning` with no obvious cause. JavaScript's `async` function at least *executes* synchronously up to the first `await`; Python's does not execute *at all* until awaited. Making `slugify` an ordinary `def` removes the trap entirely: it runs when called, returns a `str`, and cannot be accidentally left unawaited. Keep `async def` only when the body actually `await`s something (or implements an `async` protocol method the interface requires).Exercise 14 — Mini-project: clean up the Downloader¶
Anti-pattern: both, in one small realistic module · Language: TypeScript · Difficulty: ★★★★ project
This Downloader manages to commit both misuse anti-patterns plus their error leaks: a needless new Promise around a fetch chain, an async Promise executor, a pointless async on a pure helper, and a missing timeout/cancellation. Refactor it into correct, idiomatic code. Work in steps; keep it working after each.
class Downloader {
// (1) pointless async — no await, pure string work
async buildUrl(base: string, id: string): Promise<string> {
return `${base}/items/${id}`;
}
// (2) needless new Promise wrapping a fetch chain; loses network errors
fetchItem(url: string): Promise<unknown> {
return new Promise((resolve) => {
fetch(url).then((res) => res.json()).then((data) => resolve(data));
});
}
// (3) async Promise executor — rejections never reject the outer Promise
fetchMany(urls: string[]): Promise<unknown[]> {
return new Promise(async (resolve, reject) => {
const out: unknown[] = [];
for (const url of urls) {
out.push(await this.fetchItem(url)); // serial, and a reject is lost
}
resolve(out);
});
}
}
Acceptance criteria - async without await: buildUrl becomes synchronous. - Promise Constructor: fetchItem returns the fetch chain directly (no new Promise), checks res.ok, and propagates errors. - async executor: fetchMany is a plain async function (or Promise.all), and a rejection from any item rejects the result. - Add a real new Promise only where one belongs: a withTimeout over setTimeout (the legitimate case). - Each unit is independently testable.
Hint: fix one method at a time. buildUrl → drop async. fetchItem → return the chain + res.ok. fetchMany → Promise.all. Then layer a legitimate-new Promise timeout.
Solution
class Downloader {
// (1) FIXED: pure synchronous helper — no async, no Promise.
buildUrl(base: string, id: string): string {
return `${base}/items/${id}`;
}
// (2) FIXED: return the fetch chain directly; check status; errors propagate.
async fetchItem(url: string): Promise<unknown> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
return res.json();
}
// (3) FIXED: plain async, parallel, and a single rejection rejects the whole.
fetchMany(urls: string[]): Promise<unknown[]> {
return Promise.all(urls.map((url) => this.fetchItem(url)));
}
}
// The ONE legitimate new Promise: bridging setTimeout for a timeout.
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
let timer: ReturnType<typeof setTimeout>;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
});
return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
}
// usage:
// const d = new Downloader();
// const url = d.buildUrl("https://api.co", "42"); // sync, no await
// const item = await withTimeout(d.fetchItem(url), 2000);
// const all = await withTimeout(d.fetchMany([url1, url2]), 5000);
Exercise 15 — Write a misuse review checklist + lint config¶
Anti-pattern: meta (prevention) · Difficulty: ★★ medium
Misuse is cheapest to stop in review and cheaper still in the linter. Write (a) a concise reviewer checklist for these two anti-patterns, phrased as questions with a clear "if yes, push back" trigger, and (b) the lint rules that catch them automatically so humans don't have to.
Acceptance criteria - Concrete, answerable questions — not "is this clean?" - Each question names the failure mode and the fix. - Real lint rule names (ESLint / TypeScript / Python) that mechanize the checks.
Solution
**Async-misuse PR review checklist** | # | Question | If the answer is… | Then | |---|---|---|---| | 1 | Does a `new Promise(...)` wrap something that is *already* a Promise (a `.then`, an `async` call, a `fetch`)? | "Yes" | Push back: return the inner Promise / `await` it. **Promise Constructor anti-pattern** — and check for a lost rejection. | | 2 | In any `new Promise(resolve, reject)`, is `reject` actually called on every failure path? | "No" / "only `resolve`" | Push back: a failure leaves the Promise pending forever. Wire `reject` (or unwrap). | | 3 | Is an `async` function passed as a Promise executor (`new Promise(async ...)`)? | "Yes" | Push back: rejections are lost. Make it a plain `async` function; drop the `new Promise`. | | 4 | Does an `async` function contain **no** `await` (and no `for await`)? | "Yes, and the body is purely synchronous" | Push back: drop `async`. *(Exception: it normalizes a sometimes-sync API — see #5.)* | | 5 | If a no-`await` `async` is kept, is there a *stated* reason (uniform Promise return, interface contract)? | "No reason given" | Ask for the justification or remove `async`. | | 6 | When a `new Promise` *is* justified (timer/event/callback), are listeners/timers cleaned up on settle? | "No" | Push back: leaked timer/listener; clear/remove on settle. | **Lint rules that mechanize this (so review doesn't have to):**// .eslintrc — flags most of the above automatically
{
"rules": {
"no-async-promise-executor": "error", // #3: async function as executor
"no-promise-executor-return": "error", // returning a value from an executor (a smell)
"require-await": "error", // #4: async with no await
"@typescript-eslint/require-await": "error",
"@typescript-eslint/no-floating-promises": "error", // catches dropped Promises generally
"@typescript-eslint/promise-function-async": "off" // do NOT force async on Promise-returning fns
}
}
Summary¶
- The two misuse anti-patterns share a root: async ceremony that buys no concurrency. The Promise Constructor anti-pattern wraps something already asynchronous;
asyncwithoutawaitmarks something that never suspends. Both add cost, and the first routinely loses errors. - Never wrap a Promise in
new Promise. A.thenchain, anasynccall, afetch— these are already Promises. Return them (orawaitthem) and rejections propagate for free. Wrapping them re-implements Promise machinery and, if you forgetreject, leaves the Promise pending forever — the worst failure mode, because it produces no signal at all. new Promiseis the right tool for exactly one job: bridging a callback / timer / event API that has no Promise of its own (setTimeout, error-first callbacks,EventEmitter). When you do, wire bothresolveandreject, and clean up the timer/listener on settle. Exercises 5, 6, 9, 10, 11 are the legitimate cases — internalize the contrast with 1–4.- Drop
asyncwhen the body never awaits — except whenasyncdeliberately normalizes a sometimes-sync API into a singlePromiseshape (Exercise 8). The deciding question is always: does any code path await? - The
asyncPromise executor (new Promise(async ...)) is doubly wrong — redundant and an error leak — and collapses to a plainasyncfunction every time. - Mechanize the cure.
no-async-promise-executorandrequire-awaitturn both anti-patterns into build failures; leave the linter the rote checks and the reviewer the judgment calls.
Related Topics¶
junior.md— the two shapes and how to recognize them on sight.middle.md— why the constructor wrapper loses errors, and the cures applied here.find-bug.md— spot-the-misuse snippets (critical reading practice).optimize.md— more flawed implementations to make correct and idiomatic.interview.md— Q&A across all levels for job prep.- Async Anti-Patterns overview — the chapter and the other two categories (Error Handling, Execution Shape).
- Error Handling category — Swallowed Rejection and Floating Promise, the failures these misuses cause.
- Execution Shape category —
await-in-a-loop and Mixing Callbacks and Promises, near neighbors of these exercises. - Concurrency Anti-Patterns — the multi-thread sibling chapter (different failure modes).
- Functional Programming → Async — composing Promises without re-wrapping them.
In this topic