Skip to content

fix: remove async executor anti-pattern in processScreenshotWithDefaultBackground#3

Open
zbruhnke wants to merge 1 commit intoKartikLabhshetwar:mainfrom
zbruhnke:fix/promise-anti-pattern
Open

fix: remove async executor anti-pattern in processScreenshotWithDefaultBackground#3
zbruhnke wants to merge 1 commit intoKartikLabhshetwar:mainfrom
zbruhnke:fix/promise-anti-pattern

Conversation

@zbruhnke
Copy link

Summary

This PR fixes the async executor anti-pattern in processScreenshotWithDefaultBackground() which could cause silent error loss and hanging promises.

  • Refactored new Promise(async ...) anti-pattern to use proper async/await
  • Added reusable helper functions for image loading and canvas-to-data-url conversion
  • Added comprehensive test suite demonstrating the anti-pattern and the fix

The Problem

The original code used new Promise(async (resolve, reject) => { ... }), which is a well-known anti-pattern. When an async executor throws an error before the first await, the error escapes the Promise constructor and becomes an unhandled rejection:

// ❌ Anti-pattern
return new Promise(async (resolve, reject) => {
  validateInput(input);  // If this throws, promise never settles!
  const result = await asyncOp();
  resolve(result);
});

Consequences:

  • The promise hangs forever (never resolves or rejects)
  • Callers' try/catch blocks can't catch the error
  • .catch() handlers are never invoked
  • The error is silently lost as an unhandled rejection

The Fix

Refactored to use proper async/await with helper functions that wrap callback-based browser APIs:

// ✅ Correct pattern
function loadImage(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
    img.src = src;
  });
}

export async function processScreenshotWithDefaultBackground(imagePath: string): Promise<string> {
  const img = await loadImage(assetUrl);      // Errors properly propagate
  const bgImg = await loadImage(defaultBgImage);
  const canvas = createHighQualityCanvas({ ... });
  return canvasToDataUrl(canvas);
}

Benefits

Before After
~90 lines ~70 lines
4+ levels of nested callbacks Flat async/await structure
Errors could be silently lost All errors properly propagate
Difficult to test error paths Easy to test with standard try/catch

Test Plan

Added src/lib/promise-anti-pattern.test.ts with comprehensive tests:

  • Tests that pass for both implementations (basic functionality works)
  • Tests that expose the anti-pattern bug:
    • anti-pattern: sync error causes promise to hang (never settles)
    • anti-pattern: cannot use .catch() to handle sync errors
    • demonstrates lost errors - callers think everything is fine
  • Tests showing the correct pattern handles all errors properly

Run tests with:

npm test

Note: The anti-pattern demonstration tests intentionally cause unhandled rejections (that's the bug being demonstrated). Vitest reports these, which proves the anti-pattern is problematic.


🤖 Generated with Claude Code

…ltBackground

The function was using `new Promise(async (resolve, reject) => { ... })`,
which is a known anti-pattern that causes errors thrown before the first
`await` to become unhandled rejections instead of properly rejecting the
promise.

## The Problem

When an async executor throws synchronously (before any await), the error
escapes the Promise constructor and becomes an unhandled rejection:

```typescript
// Anti-pattern - errors before first await don't reject the promise
return new Promise(async (resolve, reject) => {
  validateInput(input);  // If this throws, promise never settles!
  const result = await asyncOp();
  resolve(result);
});
```

This means:
- The promise hangs forever (never resolves or rejects)
- Callers' try/catch blocks can't catch the error
- `.catch()` handlers are never invoked
- The error is silently lost as an unhandled rejection

## The Fix

Refactored to use proper async/await pattern with helper functions that
wrap callback-based APIs (Image.onload, canvas.toBlob, FileReader):

- `loadImage(src)`: Wraps Image loading in a proper Promise
- `canvasToDataUrl(canvas)`: Wraps toBlob + FileReader in a proper Promise
- Main function is now a clean async function using await

## Benefits

- All errors (sync and async) properly propagate through the promise chain
- Code reduced from ~90 lines to ~70 lines
- Flat structure instead of 4+ levels of nested callbacks
- Helper functions are reusable
- Easier to read, test, and maintain

## Testing

Added comprehensive test suite demonstrating the anti-pattern:
- Tests that pass for both old and new implementations (basic functionality)
- Tests that expose the bug (correct pattern passes, anti-pattern fails)
- Real-world scenario showing how errors get lost with the anti-pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 13, 2026

@zbruhnke is attempting to deploy a commit to the knox projects Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant