The Backend habit that is quietly slowing your entire team
The hidden backend pattern that quietly drains velocity across every project
The Backend habit that is quietly slowing your entire team
The hidden backend pattern that quietly drains velocity across every project
Photo by Mohammad Rahmani on Unsplash
There’s a habit almost every backend engineer picks up early and never questions again: returning errors bare, with no context attached.
It compiles gracefully. It passes the review. Nobody ever flags it in a PR comment.
And yet, multiplied across a codebase and a team over a few months, it’s one of the most expensive habits in software engineering — not because it breaks anything today, but because of what it costs everyone six weeks from now, at 2 a.m., staring at a log line that says nothing.
To me, this isn’t a performance problem, its a velocity problem. And velocity problems are sneaky because nobody ever opens an incident report titled “we are slow.” They just quietly are.
The habit, in code
Here’s what it looks like. Completely ordinary Go code, the kind that ships every day:
func (s *LoanService) DisburseLoan(ctx context.Context, loanID string) error {
loan, err := s.repo.GetLoan(ctx, loanID)
if err != nil {
return err
}
account, err := s.repo.GetAccount(ctx, loan.AccountID)
if err != nil {
return err
}
if err := s.ledger.Post(ctx, account.ID, loan.Amount); err != nil {
return err
}
return nil
}
Nothing here is wrong in the sense that the compiler cares about. It’s clean, it’s short, it “handles” every error.
But watch what happens the day this fails in production. Someone reports “loan disbursement failed for a customer.” The on-call engineer opens the logs and sees:
error: record not found
That’s it.
No loan ID. No indication of which of the three database calls actually failed. No hint of what record wasn’t found.
Now the engineer has to go read the source code, mentally trace the call stack, reproduce the request shape, and guess.
What should have been a two-minute log lookup becomes a thirty-minute archaeology dig — and it happens every single time this function fails, for every engineer who touches it, for as long as the code lives.
That’s the tax. It’s not paid once. It’s paid on every failure, by whoever happens to be holding the pager.
Why this habit actually spreads
The reason this pattern survives code review so easily is that return err genuinely looks correct.
It satisfies go vet, it satisfies linters, it satisfies the reviewer skimming a 400-line diff at the end of the day.
Nobody's negligent here — the cost is invisible at write-time and only shows up at debug-time, and debug-time is rarely the same person, rarely the same week, and rarely gets traced back to the original commit.
This is exactly why it’s a team velocity problem rather than an individual one. The engineer who wrote return err pays nothing.
The engineer who's on call three weeks later, for a service they didn't build, pays the full price.
The fix that costs almost nothing
The fix is not a redesign. It’s not a new framework either.
It’s one habit: wrap the error with context every time it crosses a boundary — a function boundary, a package boundary, a call to an external dependency.
func (s *LoanService) DisburseLoan(ctx context.Context, loanID string) error {
loan, err := s.repo.GetLoan(ctx, loanID)
if err != nil {
return fmt.Errorf("disburse loan: get loan %s: %w", loanID, err)
}
account, err := s.repo.GetAccount(ctx, loan.AccountID)
if err != nil {
return fmt.Errorf("disburse loan: get account %s for loan %s: %w",
loan.AccountID, loanID, err)
}
if err := s.ledger.Post(ctx, account.ID, loan.Amount); err != nil {
return fmt.Errorf("disburse loan: post ledger entry for account %s: %w",
account.ID, err)
}
return nil
}
Now, the exact same failure produces:
error: disburse loan: get account acc_9182: get loan ln_4471: record not found
That single line tells the on-call engineer which loan, which account, and which query failed, without opening a single file.
The %w verb is doing real work here too — it preserves the original error so callers can still use errors.Is or errors.As to check for a specific sentinel error further up the stack, instead of parsing strings.
var ErrAccountNotFound = errors.New("account not found")
// somewhere up the call chain
if errors.Is(err, ErrAccountNotFound) {
return status.Error(codes.NotFound, "account does not exist")
}
Wrapping and sentinel checking aren’t in tension. Wrapping gives you a legible chain for a human reading logs; errors.Is/errors.As gives you a legible chain for the program itself.
You get both, for the cost of one fmt.Errorf call per boundary.
The habit that scales, and the one that doesn’t
The trap comes in thinking of error wrapping as a nice-to-have, something you’ll “add later once things are stable.”
But this is exactly backwards. The earlier a service is in its life, the fewer people understand its internals, which means bare errors are most expensive early — right when your team can least afford the debugging tax.
By the time a service is “stable,” people have usually built tribal knowledge of its failure modes anyway, which is precisely the crutch that lets the habit survive unnoticed.
The same logic applies beyond a single service.
In distributed systems — the kind built on Temporal workflows, gRPC calls between services, or event pipelines over Redpanda — an error can cross five process boundaries before a human ever sees it.
If each hop swallows context instead of adding it, you end up with a stack trace that’s technically “handled” at every layer and useless at the top.
func (w *LoanWorkflow) Execute(ctx workflow.Context, input LoanInput) error {
err := workflow.ExecuteActivity(ctx, DisburseLoanActivity, input).Get(ctx, nil)
if err != nil {
return fmt.Errorf("loan workflow %s: disburse activity: %w", input.LoanID, err)
}
return nil
}
None of this is clever. That’s exactly why it works — it’s boring, it’s cheap, and it compounds.
The teams that move fastest aren’t the ones with the most sophisticated architecture.
They’re often the ones where a failure at 2 a.m. tells you exactly what broke, on the first read, without waking up three people to find out.
Thanks for reading. Read these too
메타데이터
- post_id
- 0778e783e14f
- slug
- the-backend-habit-that-is-quietly-slowing-your-entire-team-0778e783e14f
- url
- https://medium.com/codex/the-backend-habit-that-is-quietly-slowing-your-entire-team-0778e783e14f
- canonical_url
- https://medium.com/codex/the-backend-habit-that-is-quietly-slowing-your-entire-team-0778e783e14f
- author_url
- https://medium.com/@yaninyzwitty
- status
- ok
- fetched_at
- 2026-07-17 10:42:52