Every developer has lost an afternoon to a bug that turned out to be a one-line fix. This post covers seven common ones across frontend, backend, and DevOps, with the fix for each.
1. CORS errors: "No 'Access-Control-Allow-Origin' header"
The problem: Your frontend calls your API and the browser blocks it.
Why it happens: CORS is enforced by the browser, and the server has to opt in. Changing frontend code won't fix it.
The fix (Express):
import cors from "cors";
app.use(cors({
origin: ["http://localhost:3000", "https://yourapp.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
}));
```
**Gotcha:** If you use `credentials: true`, you can't use `origin: "*"`. List your origins explicitly. Also check that your server handles `OPTIONS` preflight requests.
## 2. The N+1 query problem
**The problem:** Your API is slow, and the logs show hundreds of near-identical queries.
```javascript
// Bad: 1 query for posts + N queries for authors
const posts = await Post.findAll();
for (const post of posts) {
post.author = await User.findByPk(post.authorId);
}
```
**The fix:** Use eager loading or a join.
```javascript
// Good: 1-2 queries total
const posts = await Post.findAll({ include: [User] });
```
**Tip:** Turn on query logging in development. N+1 problems are easy to spot once you can see the SQL.
## 3. Forgetting `await` (and unhandled promise rejections)
**The problem:** Your function "works" but returns `Promise { <pending> }`, or crashes Node with an unhandled rejection.
```javascript
// Bug: errors are swallowed, order isn't guaranteed
items.forEach(async (item) => {
await saveItem(item);
});
```
**The fix:** `forEach` doesn't wait for async callbacks. Use `for...of` for sequential work or `Promise.all` for parallel work.
```javascript
// Sequential
for (const item of items) await saveItem(item);
// Parallel
await Promise.all(items.map(saveItem));
```
Always wrap the top-level call in `try/catch`, or attach `.catch()`.
## 4. Secrets committed to Git
**The problem:** An API key or {% raw %}`.env`{% endraw %} file ends up in your repo. Bots scan public GitHub constantly, so a leaked key can be abused within minutes.
**The fix:**
1. **Rotate the key immediately.** Deleting the commit isn't enough, because the key is still in history.
2. Add `.env` to `.gitignore`.
3. Scrub history with `git filter-repo` or BFG Repo-Cleaner.
4. Add a pre-commit scanner like `gitleaks` or `trufflehog`.
```bash
# .gitignore
.env
.env.*
!.env.example
```
Commit a `.env.example` with fake values so teammates know what's required.
## 5. Docker builds that never use the cache
**The problem:** Every {% raw %}`docker build`{% endraw %} reinstalls all your dependencies.
{% raw %}
```dockerfile
# Bad: any code change invalidates the npm install layer
COPY . .
RUN npm install
```
{% endraw %}
**The fix:** Order layers from least to most frequently changed.
{% raw %}
```dockerfile
# Good: dependencies are cached until package files change
COPY package*.json ./
RUN npm ci
COPY . .
```
{% endraw %}
Add a {% raw %}`.dockerignore`{% endraw %} (with {% raw %}`node_modules`{% endraw %}, {% raw %}`.git`{% endraw %}, and {% raw %}`dist`{% endraw %}) to keep the build context small.
## 6. React: infinite re-renders and stale state
**The problem:** {% raw %}`Too many re-renders`{% endraw %}, or a {% raw %}`useEffect`{% endraw %} that runs forever.
{% raw %}
```javascript
// Bug: new object every render, so the effect re-fires every time
useEffect(() => {
fetchData(options);
}, [options]); // options = { page: 1 } defined inline
```
{% endraw %}
**The fix:** Memoize objects and functions used in dependency arrays, or depend on primitives.
{% raw %}
```javascript
const options = useMemo(() => ({ page }), [page]);
useEffect(() => { fetchData(options); }, [options]);
```
{% endraw %}
If state looks stale inside a callback, use the functional update form: {% raw %}`setCount(c => c + 1)`{% endraw %}.
## 7. Memory leaks in long-running Node.js services
**The problem:** Memory climbs until the process crashes with {% raw %}`JavaScript heap out of memory`{% endraw %}.
**Common causes:**
- Event listeners added but never removed
- Unbounded in-memory caches (a plain {% raw %}`Map`{% endraw %} that only grows)
- Timers ({% raw %}`setInterval`{% endraw %}) that are never cleared
- Closures holding references to large objects
**How to find it:**
{% raw %}
```bash
node --inspect app.js
# Open chrome://inspect, take heap snapshots, compare them
```
{% endraw %}
Take one snapshot at startup and another after load, then look at what's growing. For caches, use an LRU cache with a max size instead of a bare `Map`.
## Debugging checklist
When you're stuck, run through this:
- [ ] Can I reproduce it reliably?
- [ ] What changed since it last worked? ({% raw %}`git bisect`{% endraw %} helps)
- [ ] Am I reading the *actual* error message and stack trace?
- [ ] Have I checked the network tab, logs, and environment variables?
- [ ] Can I shrink it to a minimal reproducible example?
## Wrapping up
Most of these bugs come from a handful of root causes: misunderstanding where a rule is enforced (CORS), hidden cost (N+1), async behavior, and configuration hygiene. Learning to recognize the *category* of a problem speeds up debugging a lot.
**Which of these has cost you the most time? Share your worst debugging story in the comments.** 👇
Top comments (1)
비동기 forEach를 순차 처리와 Promise.all로 나눠 설명한 부분이 좋네요. 외부 API나 DB 쓰기 작업이라면 Promise.all도 동시에 요청을 너무 많이 보낼 수 있으니, 동시 실행 수 제한과 부분 실패 재시도 기준까지 함께 두면 운영 중 디버깅에 더 도움이 될 것 같습니다.