
Photo by Markus Spiske from Pexels
Web development is a complex process that involves various technologies, frameworks, and best practices. Even experienced developers often encounter errors that can disrupt their workflow. In this blog, we will discuss five common web development errors faced by both Senior and Junior web developer, their causes, and effective solutions to resolve them.
Why Junior Developers Face These Specific Errors
Before diving into the solutions, it's crucial to understand why these errors are so common among newcomers. Junior developers often:
Focus on making code work rather than understanding why it works
Skip reading documentation thoroughly
Copy-paste code without fully grasping its purpose
Rush to implement features without proper planning
Underestimate the importance of debugging skills
The good news? These errors are actually learning opportunities in disguise. Each mistake teaches you something valuable about programming fundamentals that will make you a stronger developer.
1. The Classic "Cannot Read Property of Undefined" Error
What It Looks Like
// This will definitely break your application
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of nullWhy This Happens to Junior Developers?
This error occurs when you try to access a property or method on something that doesn't exist (null or undefined). Junior developers often encounter this because they assume data will always be available when their code runs.
Real-World Scenario
Imagine you're building a user profile page that displays user information from an API:
// Bad approach - junior developer mistake
function UserProfile() {
const [user, setUser] = useState();
useEffect(() => {
fetchUserData().then(setUser);
}, []);
return (
<div>
<h1>{user.name}</h1> {/* This will crash! */}
<p>{user.email}</p>
</div>
);
}
Step-by-Step Solution
Step 1: Add Proper Null Checks
// Better approach with safety checks
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUserData()
.then(userData => {
setUser(userData);
setLoading(false);
})
.catch(error => {
console.error('Failed to fetch user:', error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <div>User not found</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Step 2: Use Optional Chaining (Modern JavaScript)
// Even safer with optional chaining
function UserProfile() {
const [user, setUser] = useState();
return (
<div>
<h1>{user?.name || 'Guest User'}</h1>
<p>{user?.email || 'No email provided'}</p>
<img src={user?.avatar?.url || '/default-avatar.png'} alt="Avatar" />
</div>
);
}
Prevention Tips
Always initialize state with appropriate default values
Use TypeScript for better type checking
Implement proper loading states
Use optional chaining for nested properties
2. The Mysterious CORS Error That Blocks Everything
What It Looks Like
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.Why This Happens to Junior Developers ?
CORS (Cross-Origin Resource Sharing) errors are particularly frustrating for beginners because the error message doesn't clearly explain what's wrong or how to fix it. Many junior developers first encounter this when trying to connect their frontend to an API.
Real-World Scenario
You're building a weather app that needs to fetch data from an external API:
// This will likely cause a CORS error
async function getWeatherData(city) {
try {
const response = await fetch(`https://api.weather.com/v1/weather?city=${city}`);
const data = await response.json();
return data;
} catch (error) {
console.error('CORS error:', error);
// Your app breaks here
}
}
Step-by-Step Solution
Step 1: Understanding CORS
CORS is a browser security feature that prevents websites from making requests to different domains unless explicitly allowed. It's not a bug – it's a security measure.
Step 2: Backend Solution (If You Control the Server)
// Express.js server setup
const express = require('express');
const cors = require('cors');
const app = express();
// Method 1: Allow all origins (only for development)
app.use(cors());
// Method 2: Specific origins (recommended for production)
const corsOptions = {
origin: [
'http://localhost:3000',
'https://yourproductiondomain.com'
],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
app.get('/api/weather/:city', (req, res) => {
const { city } = req.params;
// Your weather API logic here
res.json({ city, temperature: 25, condition: 'sunny' });
});
app.listen(5000, () => {
console.log('Server running on port 5000');
});
Step 3: Frontend Solution (When You Don't Control the API)
// Create a proxy server or use your backend as a middleman
async function getWeatherData(city) {
try {
// Instead of calling the external API directly:
// const response = await fetch(`https://api.weather.com/v1/weather?city=${city}`);
// Call your own backend that then calls the external API:
const response = await fetch(`http://localhost:5000/api/weather/${city}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching weather data:', error);
throw error;
}
}
Step 4: Development Proxy Solution
For React applications, you can add a proxy to your package.json:
{
"name": "weather-app",
"version": "1.0.0",
"proxy": "http://localhost:5000",
"dependencies": {
"react": "^18.0.0"
}
}
Prevention Tips
Always plan your API architecture early
Use your own backend as a proxy for external APIs
Understand browser security policies
Test with different domains early in development
3. The State Management Nightmare
What It Looks Like
// This creates infinite loops and performance issues
function ProblemComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);
// Wrong! This runs on every render
useEffect(() => {
fetchData().then(setData);
}); // Missing dependency array
// Wrong! This creates an infinite loop
useEffect(() => {
setCount(count + 1);
}, [count]);
return <div>Count: {count}</div>;
}
Why This Happens to Junior Developers?
State management is one of the most challenging concepts for new developers. They often struggle with:
Understanding when components re-render
Properly managing useEffect dependencies
Avoiding infinite loops
Managing state across multiple components
Real-World Scenario
You're building a todo list application and running into performance issues:
// Problematic todo list implementation
function TodoList() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
const [loading, setLoading] = useState(false);
// Problem 1: Missing dependency array causes infinite API calls
useEffect(() => {
fetchTodos().then(setTodos);
});
// Problem 2: Expensive filtering on every render
const filteredTodos = todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
// Problem 3: Inline functions cause unnecessary re-renders
return (
<div>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
onDelete={() => deleteTodo(todo.id)}
/>
))}
</div>
);
}
Step-by-Step Solution
Step 1: Fix useEffect Dependencies
function TodoList() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
const [loading, setLoading] = useState(false);
// Fixed: Add empty dependency array to run only once
useEffect(() => {
async function loadTodos() {
setLoading(true);
try {
const todosData = await fetchTodos();
setTodos(todosData);
} catch (error) {
console.error('Failed to load todos:', error);
} finally {
setLoading(false);
}
}
loadTodos();
}, []); // Empty array means run only once after mount
// Rest of component...
}
Step 2: Optimize Expensive Computations with useMemo
import { useState, useEffect, useMemo, useCallback } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// Memoize expensive filtering operation
const filteredTodos = useMemo(() => {
console.log('Filtering todos...'); // This should only run when todos or filter changes
return todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
}, [todos, filter]); // Only recalculate when todos or filter changes
// Memoize callback functions to prevent unnecessary re-renders
const toggleTodo = useCallback((id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const deleteTodo = useCallback((id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
}, []);
return (
<div>
<FilterButtons filter={filter} setFilter={setFilter} />
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
);
}
Step 3: Create a Memoized Child Component
import React, { memo } from 'react';
// Memoize TodoItem to prevent unnecessary re-renders
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log(`Rendering todo: ${todo.id}`); // Should only log when this specific todo changes
return (
<div className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</div>
);
});
Prevention Tips
Always include dependency arrays in useEffect
Use useMemo for expensive computations
Use useCallback for functions passed to child components
Consider using React DevTools Profiler to identify performance issues
4. The "It Works on My Machine" Deployment Disaster
What It Looks Like
# Local development - everything works fine
npm start
✓ App running on http://localhost:3000
# Production deployment - everything breaks
npm run build
✗ Module not found: Can't resolve './Component'
✗ process is not defined
✗ 404 errors for all routes
Why This Happens to Junior Developers?
Junior developers often focus solely on getting their code to work locally without considering how it will behave in different environments. Common issues include:
Case-sensitive file imports
Environment-specific dependencies
Missing build configurations
Different Node.js versions
Real-World Scenario
You've built a perfect React application locally, but deployment fails:
// Local file structure (works on Windows/macOS with case-insensitive filesystems)
src/
components/
UserProfile.jsx
pages/
HomePage.jsx
// Your import (works locally, fails on Linux servers)
import UserProfile from './components/userProfile'; // Wrong case!
import HomePage from './Pages/HomePage'; // Wrong case!
Step-by-Step Solution
Step 1: Fix Case-Sensitive Import Issues
// Before: Case doesn't match actual filenames
import UserProfile from './components/userProfile';
import HomePage from './Pages/HomePage';
// After: Exact case matching
import UserProfile from './components/UserProfile';
import HomePage from './pages/HomePage';
// Pro tip: Use absolute imports to avoid confusion
import UserProfile from 'components/UserProfile';
import HomePage from 'pages/HomePage';
Step 2: Set Up Absolute Imports Properly
// jsconfig.json (for JavaScript projects)
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"components/*": ["components/*"],
"pages/*": ["pages/*"],
"utils/*": ["utils/*"]
}
}
}
// tsconfig.json (for TypeScript projects)
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"components/*": ["components/*"],
"pages/*": ["pages/*"],
"utils/*": ["utils/*"]
}
}
}
Step 3: Environment Configuration
// config/environment.js
const config = {
development: {
API_URL: 'http://localhost:5000/api',
DEBUG: true
},
production: {
API_URL: 'https://your-api-domain.com/api',
DEBUG: false
}
};
export default config[process.env.NODE_ENV || 'development'];
// Using environment configuration
import config from 'config/environment';
async function fetchUserData(userId) {
const response = await fetch(`${config.API_URL}/users/${userId}`);
if (!response.ok) {
if (config.DEBUG) {
console.error('API Error:', response.status, response.statusText);
}
throw new Error('Failed to fetch user data');
}
return response.json();
}
Step 4: Deployment-Ready Build Script
// package.json
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"build:staging": "REACT_APP_ENV=staging npm run build",
"build:production": "REACT_APP_ENV=production npm run build"
}
}
Prevention Tips
Always test builds locally before deploying
Use consistent file naming conventions
Set up proper environment configurations
Test on different operating systems if possible
5. The Async/Await Confusion That Breaks Everything
What It Looks Like
// This looks right but doesn't work as expected
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Wrong! useEffect callback can't be async
async () => {
const userData = await fetchUser(userId);
setUser(userData);
};
}, [userId]);
// Wrong! This doesn't wait for the async operation
const handleSave = async () => {
updateUser(user);
showSuccessMessage(); // Runs before updateUser completes!
};
return <div>{user?.name}</div>;
}
Why This Happens to Junior Developers
Asynchronous programming is one of the most challenging concepts in JavaScript. Junior developers often struggle with:
Understanding the difference between promises and async/await
Proper error handling in async functions
Using async functions in React hooks
Managing loading states during async operations
Real-World Scenario
You're building a form that saves user data, but the user experience is broken:
// Problematic form implementation
function UserForm({ user, onSave }) {
const [formData, setFormData] = useState(user);
const [saving, setSaving] = useState(false);
// Problem 1: No error handling
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
await saveUser(formData); // What if this fails?
setSaving(false);
onSave(); // This might not run if saveUser fails
};
// Problem 2: Race conditions with multiple saves
const handleQuickSave = async () => {
await saveUser(formData);
showNotification('Saved!');
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={saving}>
{saving ? 'Saving...' : 'Save'}
</button>
<button type="button" onClick={handleQuickSave}>
Quick Save
</button>
</form>
);
}
Step-by-Step Solution
Step 1: Proper Async useEffect Pattern
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Create async function inside useEffect
async function loadUser() {
try {
setLoading(true);
setError(null);
const userData = await fetchUser(userId);
setUser(userData);
} catch (err) {
setError(err.message);
console.error('Failed to load user:', err);
} finally {
setLoading(false);
}
}
// Call the async function
loadUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}
Step 2: Comprehensive Error Handling
function UserForm({ user, onSave }) {
const [formData, setFormData] = useState(user);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
// Prevent multiple simultaneous saves
if (saving) return;
setSaving(true);
setError(null);
try {
await saveUser(formData);
// Show success message
showNotification('User saved successfully!', 'success');
// Call onSave callback
onSave();
} catch (err) {
// Handle different types of errors
if (err.status === 401) {
setError('You are not authorized to save this user');
} else if (err.status === 422) {
setError('Please check your input data');
} else {
setError('Failed to save user. Please try again.');
}
console.error('Save error:', err);
} finally {
setSaving(false);
}
};
const handleQuickSave = async () => {
if (saving) return; // Prevent concurrent saves
try {
setSaving(true);
await saveUser(formData);
showNotification('Quick saved!', 'success');
} catch (err) {
showNotification('Quick save failed', 'error');
} finally {
setSaving(false);
}
};
return (
<form onSubmit={handleSubmit}>
{error && (
<div className="error-message">
{error}
</div>
)}
{/* Form fields */}
<div className="form-actions">
<button
type="submit"
disabled={saving}
>
{saving ? 'Saving...' : 'Save'}
</button>
<button
type="button"
onClick={handleQuickSave}
disabled={saving}
>
Quick Save
</button>
</div>
</form>
);
}
Step 3: Custom Hook for Better Reusability
// Custom hook for async operations
function useAsync(asyncFunction, immediate = true) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(immediate);
const [error, setError] = useState(null);
const execute = useCallback(async (...params) => {
try {
setLoading(true);
setError(null);
const result = await asyncFunction(...params);
setData(result);
return result;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { data, loading, error, execute };
}
// Using the custom hook
function UserProfile({ userId }) {
const {
data: user,
loading,
error,
execute: refetchUser
} = useAsync(
() => fetchUser(userId),
true // Load immediately
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<button onClick={refetchUser}>
Refresh User Data
</button>
</div>
);
}
Prevention Tips
Always wrap async operations in try-catch blocks
Handle loading and error states explicitly
Prevent race conditions with proper state management
Create reusable patterns for common async operations
Your Path Forward: From Junior to Confident Developer
Encountering these errors doesn't make you a bad developer – it makes you a learning developer. Every senior developer has a collection of embarrassing bug stories, and these experiences are what shaped them into the problem-solvers they are today.
Building Your Error-Fighting Toolkit
1. Develop a Debugging Mindset: Instead of panicking when you see an error, get excited. Each error is a puzzle waiting to be solved.
2. Read Error Messages Carefully: Error messages often contain the exact information you need to fix the problem. Don't just copy-paste them into Google – understand what they're telling you.
3. Build Small, Test Often: Don't write 200 lines of code and then test. Write 10 lines, test, then write 10 more.
4. Use Version Control Religiously: Commit your working code frequently. When something breaks, you can always go back to the last working version.
5. Create a Personal Error Log: Keep notes about errors you've encountered and how you solved them. You'll be surprised how often similar issues come up.
Conclusion
Remember, becoming a skilled developer isn't about never making mistakes, it's about making mistakes faster, recognizing them quicker, and solving them more efficiently. The errors covered in this guide are stepping stones, not roadblocks.
Every time you encounter one of these errors and successfully resolve it, you're building the problem-solving skills that will serve you throughout your entire programming career. Embrace the debugging process, learn from each error, and before you know it, you'll be the one helping the next generation of junior developers navigate these same challenges.
Keep coding, keep learning, and remember, every expert was once a beginner who refused to give up.
LOADING...
