Sep
17
- by Warren Gibbons
- 0 Comments
Shipping avoidable bugs isn’t just frustrating-it steals evenings, derails sprints, and turns a simple change into a rabbit hole. I’ve blown a weekend chasing a missing null check while my kids, Amity and Reid, asked why Dad’s ranting at the laptop. This guide keeps it simple: guardrails that catch the usual mistakes, a workflow you can actually follow, and examples that map to the mess you see in real code. No ivory tower. Just practices that shorten feedback loops and help you ship with fewer “oops.”
TL;DR and Core Principles
coding tips worth remembering:
- Make mistakes hard to write: set up linters, formatters, types, tests, and pre-commit hooks. Let tools block bad changes early.
- Shorten feedback loops: run fast tests in seconds, commit small, review small. Catch issues while they’re cheap.
- Code for clarity first: clear names, small functions, obvious control flow. Complexity compounds bugs.
- Defend boundaries: validate inputs, handle time and money carefully, and watch for null, off-by-one, and async gotchas.
- Use a checklist every time you commit or open a PR. Pilots use them for a reason.
What you’ll get here: a simple step-by-step workflow, real examples in popular languages, tight checklists, and quick answers to common “wait, what do I do now?” moments.
Core principles that keep code safe:
- Guardrails over willpower: ESLint/Ruff/flake8/tsc/Prettier, unit tests, type hints, and pre-commit hooks catch the boring stuff reliably.
- Small is kind: small functions, small commits, small PRs. Google’s engineering review guide favors focused changes because they’re easier to reason about.
- Design for failure: network calls time out, users click back, devices sleep, clocks drift. Add retries, timeouts, and sensible defaults.
- Favor readability: write for the next person (often you in three months). Clear beats clever.
- Standard practices win: NASA/JPL’s coding rules, CERT Secure Coding, and Microsoft/Google engineering playbooks all push simplicity and defensive checks for a reason.
Step-by-Step: Avoid Common Coding Mistakes
This is the flow I use on real projects-from a tiny fix to a hairy feature. It scales, it’s quick, and it finds bugs before they find you.
-
Scope the change to one idea. Write one sentence: “Add pagination to the orders API” or “Fix null crash on login.” If your sentence has “and,” split it.
- Ask: What’s the input? What’s the output? What could break? Where does state live?
- Define a visible check: “When I hit /orders?page=2, I get 20 stable, unique results.”
-
Set up your guardrails (10 minutes).
- Formatter: Prettier for JS/TS; Black for Python. No style debates.
- Linter: ESLint 9+ (JS/TS), Ruff or flake8 (Python). Start with recommended rules.
- Types: TypeScript 5.x or Python type hints + mypy. Catch silly mismatches.
- Tests: Create or update a fast unit test. No full suite yet-just the part you touch.
- pre-commit hooks: run lint, tests, and format on staged files. Small, fast checks.
-
Write the smallest thing that works (and test it).
- Start with a failing test that describes what you want. Fix the test by writing the code.
- Keep functions tiny: under ~20 lines is a helpful heuristic, not a religion.
- Use intention-revealing names:
calculate_total_with_taxbeatscalc.
-
Handle the top 7 bug magnets as you code.
- Off-by-one: Watch inclusive vs exclusive ranges. Prefer library functions that express intent.
// JavaScript: inclusive range bug for (let i = 0; i <= arr.length; i++) { /* arr[i] undefined on last loop */ } // Safer: for (let i = 0; i < arr.length; i++) { ... } - Null/undefined: Decide if null is allowed. If yes, handle it; if no, assert early.
// TypeScript function greet(name?: string) { if (!name) return "Hello, friend"; // Intentional fallback return `Hello, ${name}`; } - Mutable defaults (Python): Never use
[]or{}as default args.# Bad def add_tag(tag, tags=[]): tags.append(tag) return tags # Good from typing import Optional, List def add_tag(tag: str, tags: Optional[List[str]] = None): tags = [] if tags is None else tags tags.append(tag) return tags - Async pitfalls: Return or await promises, don’t fire-and-forget unless you really mean it.
// Bad: forgot await const data = fetch('/api/user'); // data is a Promise, not the response // Good const res = await fetch('/api/user'); const data = await res.json(); - Time zones and DST: Store UTC, display local, and avoid adding 24h to a timestamp to mean “tomorrow.”
// JavaScript: add days safely with a library or safe util import { addDays } from 'date-fns' const tomorrow = addDays(new Date(), 1) - Money and floating point: Don’t use float for currency. Use integers (cents) or decimal types.
// JavaScript with integer cents const priceCents = 1999; // $19.99 const taxCents = Math.round(priceCents * 0.10); const totalCents = priceCents + taxCents; - Input validation: Validate at the boundary. Sanitize once, trust inside.
// Node + SQL: parameterized query prevents injection await db.query('SELECT * FROM users WHERE email = $1', [email])
- Off-by-one: Watch inclusive vs exclusive ranges. Prefer library functions that express intent.
-
Refactor while it’s fresh. Use the “Rule of Three”: the third time you paste or re-implement a pattern, extract it. Boy Scout Rule: leave the file a little cleaner than you found it.
- Split huge functions by responsibility. Name the pieces clearly.
- Remove dead code now. It won’t get easier later.
-
Run the fast test loop in seconds, not minutes.
- Keep unit tests snappy. Push slower integration tests to CI.
- Watch for flaky tests. Flakiness hides real bugs; fix or quarantine flakers fast.
-
Commit small, write a real message.
- One logical change per commit: “Add pagination param to orders API and tests.”
- Include the why, not just the what. Future-you will say thanks.
-
Open a focused PR and invite review.
- Under ~300 lines changed is easier to review well.
- Add a PR description: context, approach, trade-offs, and how to test locally.
- Ask for specific feedback: “Naming in OrderPaginator?” “Edge cases around empty results?”
-
Automate the boring checks in CI.
- CI runs lint, type-check, unit tests, security scan (e.g., Dependabot or pip-audit), and build.
- Block merge on red checks. No exceptions unless you pair-review and track the follow-up.
Examples, Checklists, and FAQs
Here are concrete fixes, tight checklists, and answers to the questions that pop up at 2 a.m. when something weird fails.
Real-world mistakes with fixes
-
Accidentally quadratic loops (slow code that looks fine):
// O(n^2) naive search inside a loop for (const id of ids) { if (!users.find(u => u.id === id)) missing.push(id) } // O(n) using a Set const userIds = new Set(users.map(u => u.id)) for (const id of ids) { if (!userIds.has(id)) missing.push(id) } -
Leaky abstraction (APIs that expose internal structures):
// Bad: caller manipulates internals function getCart() { return cartItems } // mutable shared array // Good: return copy or read-only view function getCart() { return [...cartItems] } -
Forgotten error handling with fetch/axios:
try { const res = await fetch(url, { signal, headers }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return await res.json() } catch (err) { // retry? fallback? user message? logger.warn('fetch failed', { url, err }) throw err } -
Misusing equality (JS):
// Bad if (value == 0) { /* catches '', false, '0' unintentionally */ } // Good if (value === 0) { /* exactly zero */ } -
Race conditions with async updates:
// Bad: last writer wins unpredictably await updateProfile(a) await updateProfile(b) // Better: chain or use versioning/locks await updateProfile(a).then(() => updateProfile(b)) -
Python exception swallowing:
# Bad try: risky() except Exception: pass # hides real problems # Better try: risky() except SpecificError as e: logger.warning("risky failed", exc_info=e) raise
Pre-commit checklist (30-90 seconds)
- Does the change do one thing? If not, split.
- Names clear? Any function longer than ~20 lines begging to split?
- Null/undefined accounted for? Any edge cases with empty arrays, zero, or negative numbers?
- Time, time zones, or money touched? Use UTC and integer cents/decimal types.
- Added/updated a test that would fail if this broke? Ran it locally?
- Linters clean? Formatter ran?
- Type-checks pass? (tsc/mypy)
- Logs and errors helpful, without leaking secrets?
- Any TODOs or debug prints left behind? Remove or ticket them.
- Commit message explains the why and the approach.
Pull request checklist (reviewer + author)
- Small and focused PR? If not, ask to slice it.
- Happy path works, but what about the failure path? Timeouts? Retries?
- Tests cover behavior and a nasty edge case.
- Security: input validation, parameterized queries, secrets not checked in.
- Performance: any new loops/queries that scale poorly?
- Docs updated (README, comments, ADR) if behavior changed.
Debugging decision tree (when something’s off)
- Reproduce fast and deterministically. Write a tiny failing test or script.
- Isolate the failing layer: unit (function), integration (service), or environment (config/secrets)?
- Binary search the code path: add log markers or use a debugger to find the first wrong value.
- Inspect inputs and assumptions: Are you handling nulls, time zones, and async order?
- Fix small, test, then refactor if the real root cause is bigger.
Minimum viable tests (when time is tight)
- One unit test for the core logic you just touched.
- One guard test for a known edge case (empty list, long string, invalid input).
- One integration test to prove the wiring (e.g., /orders?page=2 returns 20 results).
Tooling quick picks (2025)
- JavaScript/TypeScript: Node 20 LTS, ESLint 9, Prettier 3, TypeScript 5.x, Vitest or Jest, Playwright for e2e.
- Python: Python 3.12, Ruff (fast lint), Black, mypy or pyright, pytest, pre-commit.
- CI: GitHub Actions with matrix runs; cache dependencies; fail fast on lint/type/test.
- Security: Dependabot/Renovate, pip-audit/npm audit, secret scanning enabled.
Common environment/config mistakes to avoid
- Different env vars between dev and prod: keep a .env.example with sane defaults; document required keys.
- Assuming local time is server time: always compare UTC and convert on display only.
- Hardcoding URLs or credentials: use env vars + config files; never commit secrets.
- Version drift: pin dependency versions; use lockfiles; update on a schedule with small PRs.
Style and naming rules of thumb
- Names read like sentences:
getUserByEmail,shouldRetryPayment,isOverdraft. - Avoid abbreviations unless common (
id,urlfine;cfg,usrnot). - Functions do one thing: if you need “and,” split it.
- Reduce nesting: early returns beat deep if/else pyramids.
Mini‑FAQ
-
How do I balance speed vs quality on a deadline?
Keep the guardrails and trim everything else. Write one unit test and one edge test. Keep the PR small. Plan the follow-up refactor and actually ticket it. -
No reviewer available. What now?
Use a checklist, write a clear PR description, and do a 10-minute “rubber duck” review: walk through the code out loud, line by line. You’ll catch more than you expect. -
Which linter rules matter most?
Start with recommended sets. Add rules that catch real bugs in your stack: no-unused-vars, no-implicit-coercion, eqeqeq (JS); unused-imports, bare-except, mutable-default-arg (Python). -
Is 100% test coverage the goal?
Aim for meaningful coverage of risk, not a number. Cover core logic, tricky branches, and failure paths. Integration tests for critical flows beat chasing every line. -
How do I get better at debugging?
Practice binary search on code paths, learn your debugger shortcuts, and force a failing test for every bug you fix. Keep a “bug diary” with pattern notes-you’ll see repeats.
Next steps by persona
-
New grad or beginner: Set up a template repo with lint, format, tests, and CI. Learn your linter errors until they’re second nature. Pair once a week if you can.
-
Solo indie dev: Pre-commit hooks + tiny PRs, even if you’re the only reviewer. Schedule a weekly “maintenance hour” to update deps and kill flakiness.
-
Backend dev wrangling async: Add timeouts, retries with backoff, and idempotent handlers. Log correlation IDs. Test one failure case per integration.
-
Frontend dev: TypeScript strict mode, handle loading/error states first, and write tests for components with no props and with edge props.
-
Data/ML: Fix seeds for reproducibility, validate input ranges, and log metrics with versions of code/data. Treat preprocessing as production code.
Credible references to explore
- Google’s Engineering Practices: small, focused changes and pragmatic reviews.
- NASA/JPL Institutional Coding Standard: simplicity, defensive programming.
- CERT Secure Coding Standard: input validation, safe APIs, error handling.
- Stack Overflow Developer Survey 2024: debugging and code maintenance take a big slice of dev time-guardrails pay off.
Building software in 2025 still rewards the boring basics. Keep changes small, let tools guard you, test the parts you touch, and use a simple checklist before you ship. You’ll spend less time firefighting and more time making the thing better. That’s the whole point.