error-handling
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.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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
- Identify the error handling issue type
- Determine if the operation should fail loudly or gracefully
- Apply the appropriate fix pattern
- 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();
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-28
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!