Skip to content
Shamsher.
Go back

Welcome — and how I debug (the detective method)

Debugging

This is the first post on my dev notes blog. I’m starting it to capture the things I learn day to day — the debugging sessions, the small realizations, the fixes that finally worked. Mostly for my future self, but you’re welcome to read along.

Let me kick it off with the single habit that has saved me the most time.

Table of contents

Open Table of contents

Treat every bug like a crime scene

When something breaks, the temptation is to start changing code immediately. You have a theory — “it’s probably the cache” — and a beautiful theory is seductive. But a theory that isn’t backed by evidence leads to random fixes that happen to work once and break again next week.

So I follow a simple loop, borrowed from detective work:

The detective debugging method: state the crime, form a theory, collect evidence, let evidence convict, fix the root cause

  1. State the crime. What exactly is wrong? Be precise. “It’s slow” is not a crime; “the /orders endpoint takes 4s when it used to take 200ms” is.
  2. Form a theory. What could cause this? List a few suspects.
  3. Collect evidence. Add minimal, targeted logging or measurement. Do not fix anything yet.
  4. Let the evidence convict. Only when the data proves the theory do you make the change.
  5. Fix the root cause, not the symptom.

A worked example

Say an API call intermittently returns stale data. The seductive theory is “the cache TTL is too long.” Tempting to just lower it and move on. Instead, collect evidence first:

// Evidence-gathering, not a fix.
// Log the cache key, whether it was a hit, and how old the entry is.
function getOrder(id: string): Order {
  const key = `order:${id}`;
  const entry = cache.get(key);

  console.debug("[cache]", {
    key,
    hit: Boolean(entry),
    ageMs: entry ? Date.now() - entry.storedAt : null,
  });

  if (entry) return entry.value;

  const fresh = db.fetchOrder(id);
  cache.set(key, { value: fresh, storedAt: Date.now() });
  return fresh;
}

Run it, reproduce the bug, read the logs. Maybe you discover the cache key collides across users — a completely different crime than “TTL too long.” Now the fix is obvious and correct, and you didn’t waste an afternoon tuning a TTL that was never the problem.

The fix changes because the theory was beautiful but unproven. Evidence first.

Why fail fast helps here

A related habit: throw errors at the boundary instead of silently papering over bad state.

function assertOrder(value: unknown): asserts value is Order {
  if (!value || typeof value !== "object" || !("id" in value)) {
    throw new Error(`Expected an Order, got: ${JSON.stringify(value)}`);
  }
}

When preconditions aren’t met, you want to know immediately, at the source — not three layers downstream where the stack trace is useless. Fast failure turns a mystery into a labeled clue.

What’s next

That’s the method. Future posts will be the actual cases — the bug, the theory, the evidence, the fix. If a post helps you avoid one wrong fix, it earned its keep.

Thanks for reading.


Share this post:

Previous Post
Interactive components in MDX — how this blog embeds live demos
Next Post
Integrating Sitecore XM Cloud and Next.js with a Chinese reverse proxy