Claude Code Plugins

Community-maintained marketplace

Feedback

Fixes error handling issues including floating promises, silent catches, and throwing non-Error objects. Use when encountering @typescript-eslint/no-floating-promises, silent catch blocks, or throw statement issues.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name error-handling
description Fixes error handling issues including floating promises, silent catches, and throwing non-Error objects. Use when encountering @typescript-eslint/no-floating-promises, silent catch blocks, or throw statement issues.

Error Handling Fixes

Fixes for proper error handling in TypeScript/JavaScript. These issues can cause silent failures, lost stack traces, and unhandled promise rejections that crash Node.js.

Quick Start

  1. Identify the error handling issue type
  2. Determine if the operation should fail loudly or gracefully
  3. Apply the appropriate fix pattern
  4. Verify error paths are tested

Priority Matrix

Issue Priority Impact
Floating promises P0 Crashes Node.js on unhandled rejection
Silent catch blocks P1 Hides bugs, makes debugging impossible
Throwing non-Error P1 Loses stack traces, breaks error handling

Workflows

Floating Promises (#11 - 20 occurrences)

Detection: @typescript-eslint/no-floating-promises

Pattern: Promise returned but not awaited or handled.

// PROBLEM - floating promise
initializeDatabase();
startServer();

What Happens: If the promise rejects, Node.js crashes with "UnhandledPromiseRejection".

Fix Strategy 1: Sequential startup with top-level handler.

// SOLUTION - for startup sequences
async function main(): Promise<void> {
  await initializeDatabase();
  await startServer();
}

main().catch((error: unknown) => {
  console.error('Startup failed:', error);
  process.exit(1);
});

Fix Strategy 2: Fire-and-forget with explicit void and catch.

// SOLUTION - for intentional fire-and-forget
void logAnalytics(event).catch((error: unknown) => {
  // Analytics failure should not crash the app
  debug('Analytics failed', { error });
});

Decision Guide:

Scenario Fix
Must succeed for app to work await + let crash or handle
Nice-to-have, app works without void + catch + log
Event loop top-level Wrap in main() with catch

Why: Floating promises hide errors. Always handle explicitly. Use void to signal intent.


Silent Catch Blocks (#22 - 8 occurrences)

Detection: Catch block with unused error variable (_error or _e).

Pattern: Error caught but swallowed without logging.

// PROBLEM - silent catch
try {
  await saveToCache(data);
} catch (_error) {
  // Swallowed silently
}

Fix Strategy: Log with context, then handle gracefully.

// SOLUTION - log and handle
try {
  await saveToCache(data);
} catch (error: unknown) {
  debug('Cache save failed, continuing without cache', {
    error: error instanceof Error ? error.message : String(error),
    dataKey: data.key,
  });
  // Operation continues - cache is optional
}

Logging Template:

catch (error: unknown) {
  logger.warn('Operation failed', {
    operation: 'saveToCache',
    error: error instanceof Error ? error.message : String(error),
    stack: error instanceof Error ? error.stack : undefined,
    context: { key: data.key, size: data.size },
  });
  // Decide: re-throw, return default, or continue
}

Decision Guide:

Error Type Action
Network timeout Log, retry with backoff, or return cached
Validation error Log, return error to caller
File not found Log, return null or create
Unknown Log with full context, re-throw or fail

Why: Silent catches hide bugs. Even expected errors should be logged for debugging.


Throwing Non-Errors (#16 - 8 occurrences)

Detection: @typescript-eslint/only-throw-error

Pattern: Throwing strings or plain objects.

// PROBLEM - throwing non-Error
throw 'Something went wrong';
throw { code: 'INVALID', message: 'Bad input' };

Fix Strategy 1: Use Error objects.

// SOLUTION - Error objects
throw new Error('Something went wrong');

Fix Strategy 2: Custom error classes for structured errors.

// SOLUTION - custom error class
class ValidationError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly field?: string
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

throw new ValidationError('Bad input', 'INVALID', 'email');

Error Class Hierarchy:

// Base application error
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class ValidationError extends AppError {
  constructor(message: string, public readonly field?: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }
}

class AuthenticationError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 'AUTHENTICATION_ERROR', 401);
  }
}

Why: Error objects have stack traces. Custom classes enable instanceof checks. String throws lose debugging context.


Scripts

Detect Error Handling Issues

node scripts/detect-error-issues.js /path/to/src

Fix Floating Promises (Semi-Automated)

node scripts/fix-floating-promises.js /path/to/file.ts

Adds void prefix and .catch() handler to identified floating promises.


Error Handling Patterns

Try-Catch-Finally

let resource: Resource | null = null;
try {
  resource = await acquireResource();
  await processResource(resource);
} catch (error: unknown) {
  logger.error('Resource processing failed', { error });
  throw error; // Re-throw after logging
} finally {
  if (resource) {
    await resource.release();
  }
}

Result Type Pattern

For operations where errors are expected and should be handled:

type Result<T, E = Error> =
  | { success: true; value: T }
  | { success: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await database.findUser(id);
    if (!user) {
      return { success: false, error: new NotFoundError('User', id) };
    }
    return { success: true, value: user };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error(String(error)),
    };
  }
}

// Usage
const result = await fetchUser('123');
if (!result.success) {
  // Handle error case explicitly
  console.error(result.error.message);
  return;
}
// TypeScript knows result.value is User here
console.log(result.value.name);

Error Boundary Pattern (for frameworks)

function withErrorBoundary<T extends (...args: never[]) => Promise<unknown>>(
  fn: T,
  errorHandler: (error: unknown) => void
): T {
  return (async (...args: Parameters<T>) => {
    try {
      return await fn(...args);
    } catch (error) {
      errorHandler(error);
      throw error;
    }
  }) as T;
}

Testing Error Paths

Testing Async Errors

describe('fetchUser', () => {
  it('throws NotFoundError for missing user', async () => {
    await expect(fetchUser('nonexistent'))
      .rejects
      .toThrow(NotFoundError);
  });

  it('includes user ID in error message', async () => {
    await expect(fetchUser('123'))
      .rejects
      .toThrow('User not found: 123');
  });
});

Testing Error Properties

it('ValidationError has correct properties', () => {
  const error = new ValidationError('Invalid email', 'email');

  expect(error).toBeInstanceOf(Error);
  expect(error).toBeInstanceOf(ValidationError);
  expect(error.name).toBe('ValidationError');
  expect(error.code).toBe('VALIDATION_ERROR');
  expect(error.statusCode).toBe(400);
  expect(error.field).toBe('email');
  expect(error.stack).toBeDefined();
});

Common Anti-Patterns

Catching and Ignoring

// BAD
try { await risky(); } catch { }

// GOOD
try {
  await risky();
} catch (error) {
  logger.debug('Risky operation failed (expected)', { error });
}

Catching Too Broadly

// BAD - catches everything including programming errors
try {
  const user = await fetchUser(id);
  return processUser(user);
} catch {
  return null;
}

// GOOD - catch specific, re-throw unknown
try {
  const user = await fetchUser(id);
  return processUser(user);
} catch (error) {
  if (error instanceof NotFoundError) {
    return null;
  }
  throw error; // Re-throw unexpected errors
}

Nested Try-Catch

// BAD - hard to follow
try {
  try {
    await step1();
  } catch {
    await recovery1();
  }
  try {
    await step2();
  } catch {
    await recovery2();
  }
} catch {
  // ??
}

// GOOD - extract to functions
async function safeStep1() {
  try {
    await step1();
  } catch (error) {
    logger.warn('Step 1 failed, using recovery', { error });
    await recovery1();
  }
}

await safeStep1();
await safeStep2();