Thrown errors and unhandled promise rejections must be intentional and represent exceptional conditions, not part of the normal "happy path" of code execution.
Do NOT use errors as a mechanism for normal control flow:
// BAD: Using errors for expected conditions
function getValue(key) {
const value = map.get(key);
if (!value) throw new Error('Key not found'); // Expected case!
return value;
}
// GOOD: Return a sentinel or optional value
function getValue(key) {
return map.get(key) ?? null;
}Errors should be thrown only for:
-
Unreachable code paths (exhaustiveness checks):
switch (type) { case 'a': return handleA(); case 'b': return handleB(); default: throw new Error('unreachable'); }
-
Invalid invariants (programmer errors, not user errors):
if (!this.args) throw new Error('messaging requires args to be set'); if (!(manager instanceof VideoOverlay)) throw new Error('invalid arguments');
-
Missing required dependencies:
if (!_messagingModuleScope) throw new Error('Messaging not initialized');
-
Never leave promises unhandled - always attach
.catch()or usetry/catchwithawait -
Use
Promise.reject()only when mimicking browser API behavior (e.g.,web-compat.jsreturningDOMException):// Mimicking native Share API behavior if (!canShare(data)) return Promise.reject(new TypeError('Invalid share data'));
-
Handle async errors explicitly:
// GOOD: Explicit error handling await this.setUserChoice(choice).catch((e) => console.error('error setting user choice', e)); // GOOD: Swallow expected failures intentionally await fetchOptional().catch(() => {}); // Comment explaining why this is OK
Use assertion functions for type/state validation that throws on failure:
/**
* @returns {asserts event is CustomEvent<{kind: string, data: any}>}
*/
function assertCustomEvent(event) {
if (!('detail' in event)) throw new Error('none-custom event');
if (typeof event.detail.kind !== 'string') throw new Error('custom event requires detail.kind to be a string');
}- Error messages should clearly indicate what went wrong and why it's a bug
- Include context that helps debugging:
throw new Error(`'${method.toString()}' is not a method of feature '${this.name}'`);
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Empty catch blocks | Silently swallows errors | Log or re-throw with context |
Throwing for expected null/undefined |
Using errors as control flow | Return optional/sentinel values |
| Unhandled promise chains | Leads to unhandled rejections | Add .catch() handler |
| Generic error messages | Hard to debug | Include specific context |
| Catching and ignoring all errors | Masks bugs | Catch specific error types |
The codebase enforces:
require-await- async functions must use awaitpromise/prefer-await-to-then- prefer async/await over.then()@typescript-eslint/await-thenable- only await promises
- Errors = exceptional conditions (bugs, invariant violations, unreachable code)
- Errors ≠ expected conditions (missing data, user input validation, optional features)
- Every promise rejection must be handled or intentionally propagated