Common Gotchas and Best Practices
This guide covers common programming pitfalls and anti-patterns that developers often encounter, along with best practices to avoid them. Each section is explained in a beginner-friendly way with clear examples of what not to do and how to do it right.
Quick Navigation
Use the table of contents above to jump to specific sections, or expand/collapse categories below to focus on specific topics.
Core JavaScript & TypeScript
Type System & Language Features
Type System Details
Magic Strings and Numbers
The Problem
Using hardcoded strings and numbers throughout your code makes it difficult to maintain and prone to typos.
// ❌ Bad: Magic strings and numbers
function calculateDiscount(price) {
if (status === 'premium') {
// Magic string
return price * 0.2; // Magic number (20% discount)
}
return price * 0.1; // Magic number (10% discount)
}
// ✅ Good: Use constants with meaningful names
const CUSTOMER_TYPES = {
PREMIUM: 'premium',
REGULAR: 'regular',
} as const;
const DISCOUNT_RATES = {
[CUSTOMER_TYPES.PREMIUM]: 0.2,
[CUSTOMER_TYPES.REGULAR]: 0.1,
} as const;
function calculateDiscount(price: number, status: keyof typeof CUSTOMER_TYPES) {
return price * DISCOUNT_RATES[status];
}
Type Coercion Confusion
The Problem
JavaScript's automatic type coercion can lead to unexpected results.
// ❌ Bad: Relying on type coercion
if (userId == '123') {
} // Loose equality
if (someValue == null) {
} // Ambiguous null check
// ✅ Good: Use strict equality and explicit type checks
if (userId === '123') {
} // Strict equality
if (someValue === null || someValue === undefined) {
} // Explicit null/undefined check
Additional Type System Gotchas
// ❌ Bad: Unsafe type assertions
const value = someValue as any as SpecificType;
// ✅ Good: Type guards and runtime checks
function isSpecificType(value: unknown): value is SpecificType {
return value !== null && typeof value === 'object' && 'requiredProp' in value;
}
Generic Constraints
// ❌ Bad: Loose generic constraints
function process<T>(value: T) {
value.someMethod(); // Error: method might not exist
// ✅ Good: Proper constraints
function process<T extends { someMethod: () => void }>(value: T) {
value.someMethod(); // Safe
}
Functions & Methods
Function Details
Function Parameter Mutation
The Problem
Modifying function parameters can lead to unexpected side effects and make code harder to debug.
// ❌ Bad: Mutating function parameters
function processUser(user) {
user.lastLogin = new Date(); // Mutates the original object
return user;
}
// ✅ Good: Create new objects instead of mutating
function processUser(user) {
return {
...user,
lastLogin: new Date(),
};
}
Additional Function Gotchas
// ❌ Bad: Mutable default parameters
function createUser(config = {}) {
config.timestamp = Date.now(); // Mutates the default object
}
// ✅ Good: Immutable defaults
function createUser(config = {}) {
return {
...config,
timestamp: Date.now(),
};
}
Method Binding
// ❌ Bad: Lost context
class Handler {
handle() {
/* ... */
}
setup() {
element.addEventListener('click', this.handle); // 'this' will be wrong
}
}
// ✅ Good: Proper binding
class Handler {
handle = () => {
/* ... */
}; // Arrow function preserves context
setup() {
element.addEventListener('click', this.handle);
}
}
Testing & Quality Assurance
Test Organization
Testing Details
Testing Anti-Patterns
The Problem
Common testing mistakes that lead to unreliable or hard-to-maintain tests.
// ❌ Bad: Brittle selectors in tests
test('submits form', async () => {
await page.click('button.submit-btn.primary'); // Fragile CSS selector
});
// ✅ Good: Resilient selectors
test('submits form', async () => {
await page.getByRole('button', { name: 'Submit' }); // Resilient to styling changes
});
// ❌ Bad: Time-based tests
test('debounce works', async () => {
await someAction();
await new Promise((resolve) => setTimeout(resolve, 1000)); // Arbitrary delay
expect(result).toBe(expected);
});
// ✅ Good: Event-based tests
test('debounce works', async () => {
await someAction();
await waitForCondition(() => result === expected); // Wait for actual condition
});
Additional Testing Gotchas
// ❌ Bad: Tests affecting each other
let globalTestData = [];
beforeEach(() => {
globalTestData.push('test');
});
// ✅ Good: Isolated test data
const createTestData = () => ['test'];
beforeEach(() => {
const testData = createTestData();
});
Snapshot Testing Abuse
// ❌ Bad: Large, brittle snapshots
it('renders component', () => {
expect(render(<ComplexComponent />)).toMatchSnapshot();
});
// ✅ Good: Focused, meaningful snapshots
it('renders specific elements', () => {
const { getByRole } = render(<ComplexComponent />);
expect(getByRole('button')).toMatchSnapshot();
});
Package Management & Dependencies
Dependency Management
Dependency Details
Package Version Conflicts
The Problem
Inconsistent or conflicting package versions can cause subtle bugs and compatibility issues.
// ❌ Bad: Loose version constraints
{
"dependencies": {
"lodash": "^4.0.0", // Could get incompatible updates
"react": "*" // Any version is dangerous
}
}
// ✅ Good: Explicit version constraints
{
"dependencies": {
"lodash": "4.17.21", // Exact version
"react": "~18.2.0" // Patch updates only
}
}
Additional Package Management Gotchas
// ❌ Bad: Ignoring peer dependencies
{
"dependencies": {
"react": "18.0.0",
"some-plugin": "^1.0.0" // Requires React 17
}
}
// ✅ Good: Explicit peer dependencies
{
"dependencies": {
"react": "17.0.2",
"some-plugin": "^1.0.0"
},
"peerDependencies": {
"react": "^17.0.0"
}
}
Development Dependencies
// ❌ Bad: Development tools in dependencies
{
"dependencies": {
"jest": "^29.0.0",
"typescript": "^5.0.0"
}
}
// ✅ Good: Proper dependency categorization
{
"devDependencies": {
"jest": "^29.0.0",
"typescript": "^5.0.0"
}
}
Styling & CSS
CSS Best Practices
CSS Details
CSS Specificity Wars
The Problem
Escalating CSS specificity makes styles hard to maintain and override.
// ❌ Bad: Specificity escalation
.header .nav .list .item .link { // Too specific
color: blue;
}
#main-content div.container .button { // Mixed specificity
background: red;
}
// ✅ Good: Flat specificity with BEM
.nav__item { // Single class, clear hierarchy
color: blue;
}
.button--primary { // Modifier pattern
background: red;
}
CSS-in-JS Performance
The Problem
Inefficient CSS-in-JS implementations can cause performance issues.
// ❌ Bad: Dynamic styles in render
function Component() {
return (
<div
style={{ // New object created every render
color: 'blue',
padding: '20px'
}}
>
Content
</div>
);
}
// ✅ Good: Memoized styles
const styles = { // Object created once
container: {
color: 'blue',
padding: '20px'
}
};
function Component() {
return (
<div style={styles.container}>
Content
</div>
);
}
Performance & Optimization
Performance Patterns
Performance Details
Memory Leaks
The Problem
Not cleaning up resources can lead to memory leaks and performance issues.
// ❌ Bad: Resources not cleaned up
class VideoPlayer {
constructor() {
this.video = document.createElement('video');
this.stream = navigator.mediaDevices.getUserMedia({ video: true });
}
}
// ✅ Good: Proper resource cleanup
class VideoPlayer {
constructor() {
this.video = document.createElement('video');
this.stream = null;
}
async start() {
this.stream = await navigator.mediaDevices.getUserMedia({ video: true });
this.video.srcObject = this.stream;
}
stop() {
if (this.stream) {
this.stream.getTracks().forEach((track) => track.stop());
this.stream = null;
}
this.video.srcObject = null;
}
}
Event Handler Optimization
// ❌ Bad: New function every render
function Component() {
return (
<button onClick={() => handleClick()}>
Click me
</button>
);
}
// ✅ Good: Memoized handler
function Component() {
const handleClick = useCallback(() => {
// Handle click
}, []);
return <button onClick={handleClick}>Click me</button>;
}
Security & Error Handling
Security Best Practices
Security Details
Security Vulnerabilities
The Problem
Common security oversights can expose your application to attacks.
// ❌ Bad: Security vulnerabilities
const userInput = '<script>alert("XSS")</script>';
element.innerHTML = userInput; // XSS vulnerability
// ✅ Good: Sanitize input and use safe alternatives
import DOMPurify from 'dompurify';
element.textContent = userInput; // Safe from XSS
// Or if HTML is needed:
element.innerHTML = DOMPurify.sanitize(userInput);
Error Boundaries
The Problem
Not implementing proper error boundaries can crash entire applications.
// ❌ Bad: No error boundaries
function App() {
return (
<div>
<ComponentThatMightCrash />
</div>
);
}
// ✅ Good: Implement error boundaries
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<ComponentThatMightCrash />
</ErrorBoundary>
);
}
State Management & Data Flow
State Management Patterns
State Management Details
Global State Anti-patterns
The Problem
Improper state management can lead to unpredictable behavior and maintenance nightmares.
// ❌ Bad: Global state mutations
window.globalState = {
user: null,
settings: {},
};
// ✅ Good: Centralized state management
import { createStore } from '@reduxjs/toolkit';
const store = createStore({
reducer: {
user: userReducer,
settings: settingsReducer,
},
});
Prop Drilling
The Problem
Passing props through multiple levels of components makes code hard to maintain.
// ❌ Bad: Prop drilling through multiple levels
function GrandParent({ user }) {
return <Parent user={user} />;
}
function Parent({ user }) {
return <Child user={user} />;
}
function Child({ user }) {
return <div>{user.name}</div>;
}
// ✅ Good: Context or state management
const UserContext = React.createContext();
function GrandParent() {
return (
<UserContext.Provider value={user}>
<Parent />
</UserContext.Provider>
);
}
function Child() {
const user = useContext(UserContext);
return <div>{user.name}</div>;
}
Async Programming & Concurrency
Async Patterns
Async Details
Race Conditions
The Problem
Async operations can lead to race conditions when not properly handled.
// ❌ Bad: Race condition in async operations
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
currentUser = await response.json(); // Global state mutation
}
// ✅ Good: Race condition prevention
let currentRequest = null;
async function fetchUserData(userId) {
const request = fetch(`/api/users/${userId}`);
currentRequest = request; // Track current request
const response = await request;
if (currentRequest === request) {
// Only update if this is still the current request
return await response.json();
}
}
Promise Chain Management
The Problem
Complex promise chains can become hard to maintain and debug.
// ❌ Bad: Nested promise chains
fetchUser().then((user) => {
return fetchPosts(user.id).then((posts) => {
return fetchComments(posts[0].id).then((comments) => {
// Deep nesting
});
});
});
// ✅ Good: Async/await with proper error handling
async function fetchUserData() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
handleError(error);
}
}
Code Organization & Architecture
Architecture Patterns
Architecture Details
Component Organization
The Problem
Poor component organization leads to maintenance difficulties and code duplication.
// ❌ Bad: Mixed concerns in components
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser().then(data => {
setUser(data);
setLoading(false);
});
}, []);
return (
<div>
{loading ? <Spinner /> : <UserData user={user} />}
</div>
);
}
// ✅ Good: Separated concerns
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
return { user, loading };
}
function UserProfile({ userId }) {
const { user, loading } = useUser(userId);
if (loading) return <Spinner />;
return <UserData user={user} />;
}
File Structure
The Problem
Inconsistent file organization makes it hard to locate and maintain code.
// ❌ Bad: Mixed file organization
/src
/components
UserProfile.tsx
userProfile.css
userProfileTypes.ts
userProfileUtils.ts
// ✅ Good: Feature-based organization
/src
/features
/user-profile
/components
UserProfile.tsx
/hooks
useUser.ts
/types
types.ts
/utils
formatters.ts
index.ts
Development Workflow
Best Practices
Workflow Details
Version Control
The Problem
Poor version control practices can lead to confusion and lost work.
// ❌ Bad: Large, mixed commits
// Single commit with multiple unrelated changes
git commit -m "Updated user profile, fixed bug in cart, added new feature"
// ✅ Good: Atomic commits with clear messages
git commit -m "feat(user): add profile image upload"
git commit -m "fix(cart): handle empty cart state"
git commit -m "feat(auth): implement password reset"
Code Review Process
The Problem
Ineffective code review practices can let bugs slip through.
// ❌ Bad: Large, monolithic PRs
// PR with 50+ files changed, multiple features
// ✅ Good: Focused, single-purpose PRs
// PR for single feature with clear scope
Visual Learning Aids
Common Gotchas Flow Chart
Anti-Pattern Impact Matrix
Interactive Learning
Try It Yourself
Exercise 1: Find the Gotcha
// What's wrong with this code?
function processData(data) {
if (data == null) return;
if (data.length == 0) return;
for (var i = 0; i < data.length; i++) {
setTimeout(() => {
console.log(data[i]);
}, 100);
}
}
Exercise 2: Fix the Pattern
// How would you improve this?
class DataManager {
static instance;
data = [];
static getInstance() {
if (!DataManager.instance) {
DataManager.instance = new DataManager();
}
return DataManager.instance;
}
}
Best Practices Summary
Quick Reference
Keep these principles in mind while coding:
- Use constants for magic strings and numbers
- Prefer strict equality (
===
) over loose equality (==
) - Avoid mutating function parameters
- Minimize global state usage
- Use async/await instead of nested callbacks
- Always clean up resources and event listeners
- Implement proper error handling
- Optimize code only after measuring performance
- Write descriptive error messages
- Follow consistent naming conventions
- Use early returns to avoid deep nesting
- Handle dates consistently with proper timezone awareness
- Implement security best practices
- Centralize state management
- Use established libraries for complex functionality
- Implement proper error boundaries
- Be consistent with async patterns
- Batch DOM updates for better performance
- Always clean up resources
- Leverage TypeScript features effectively :::
Additional Resources
Contributing
Help Improve This Guide
Found a mistake or want to contribute? Check our contribution guidelines.