Retry, But Smarter

Ritt, the retry beaver
Ritt — retry, but smarter

There's a version of trying again that looks a lot like stubbornness: the same input, the same code path, the same wall. Hit enter. Wait. Hit enter again. If you're lucky, nothing breaks worse. That's not retrying. That's looping while pretending the loop will eventually end differently. This post is about the other kind — the kind that actually gets somewhere.

flowchart LR
  A[stuck] --> B{understand why?}
  B -- no --> C[retry, but smarter]
  C --> B
  B -- yes --> D[unstuck]

The Error Is a Messenger

When something fails, it doesn't just fail randomly. It fails at a specific point, with a specific message, under specific conditions. The error is telling you exactly where the contract broke.

I used to treat errors like noise — obstacles between me and a working program. Then at some point I flipped that: the error is the most accurate piece of feedback in the system. Nothing in the browser console is editorializing. It just says what happened.

The stack trace is not the enemy. It's the most honest thing in the room.

What "Different Conditions" Actually Means

Retrying isn't just running the same thing again. A smarter retry changes at least one of these:

  • What you know — you read the error and understand it better than before
  • How you query — you change the input, the boundary, or the assumption
  • When you retry — you wait, and let state settle, before trying again

If you change none of those, you're not retrying. You're rerunning.

Exponential Backoff: The Retry Pattern That Respects Time

The canonical "smarter retry" in code is exponential backoff. You don't hammer an endpoint — you wait a little, then a little more, then a little more, with some noise added so a thundering herd of clients doesn't all retry at the same instant.

ts
async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 4,
  baseDelay = 200,
): Promise<T> {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      attempt++;
      if (attempt >= maxAttempts) throw err;
      const jitter = Math.random() * baseDelay;
      const delay = baseDelay * 2 ** (attempt - 1) + jitter;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

The pattern is mechanical, but the insight underneath is not: backing off is an acknowledgment that the problem might not be yours. The server might be momentarily overloaded. The network blip might clear in 400ms. You're not failing harder — you're giving the system a chance to recover.

The Learning Equivalent

I apply the same logic outside code. When I'm stuck on a concept — say, how the browser constructs the render tree — brute-forcing through the same documentation page isn't retrying smarter. It's running the same fn() that already returned an error.

What actually works:

  1. Stop and write down what I think I understand.
  2. Find the exact point where my model breaks.
  3. Go one layer deeper — not broader — and read only that.
  4. Come back to the original thing with a cleaner mental model.

That's backoff with exponential gain. Each round, I return to the problem knowing more than I did before.

What I Actually Change Between Attempts

Here's the comparison I find useful:

AxisNaive retrySmarter retry
What you readSame paragraph againOne level deeper — the spec, the source
How fastImmediatelyAfter a pause — let it settle
What you verify"Does it feel familiar?""Can I explain it cold?"
Signal you useFrustrationThe specific part that didn't make sense
How I decide when to go deeper vs. move on

There's a heuristic I use. Ask: "If someone asked me to explain this right now, where would I get stuck?" If the stuck point is a vocabulary gap, go one layer deeper. If the stuck point is a detail that won't matter for the next six months, note it and move on — you'll encounter it again with more context.

The key is to be honest about which one it is. I've spent two hours on a detail that didn't matter and called it rigor. It wasn't.

The Shape of a Good Retry

Across both code and learning, a well-formed retry has three parts:

  • Pause — even briefly — before going again
  • Inspect the failure signal for the actual information it contains
  • Change at least one thing on the next attempt

And on the learning side, an ordered ritual I actually follow:

  1. Attempt the thing.
  2. Hit the wall. Note the exact wall, not just "this is hard."
  3. Take five minutes away.
  4. Come back with a different source or a different angle.
  5. Attempt again — now with updated priors.

That talk is a good example of a concept I've known about for years that I didn't actually understand until I watched it slowly and made myself explain each step before moving to the next. Event loop, call stack, task queue — it all clicked when I stopped scanning and started watching one segment at a time.

Closing

Failure is not the problem. The same failure, again, unchanged — that's the problem. The error message is data. The blocked feeling is a signal. Use both.

Try again. Just not the same way. retry, but smarter. 🔁