DEV Community

Cover image for The Silent Bug That Cost $28,000: Why Empty Catch Blocks Are the Most Expensive Code You'll Ever Write
Alexandr Bandurchin for GetLogFlow

Posted on

The Silent Bug That Cost $28,000: Why Empty Catch Blocks Are the Most Expensive Code You'll Ever Write

A checkout flow silently fails for 12% of users. No errors in Sentry. No alerts firing. HTTP 200 everywhere. Users click "Pay" and nothing happens. For two weeks.

This isn't a hypothetical — it's a pattern that plays out at startups every month. Here's the anatomy of how it happens and the logging setup that prevents it.

The anatomy of a silent failure

A payment service calls Stripe, gets a response, and publishes an event to a message queue. The consumer picks it up and updates the order status. Straightforward architecture.

Then Stripe adds a new field to their response object. The serializer chokes on a BigInt value. An exception is thrown. The catch block swallows it. The function returns undefined.

catch (err) {
  // TODO: handle this
  return undefined
}
Enter fullscreen mode Exit fullscreen mode

The order stays in "pending" forever. The user sees a spinner. No error is logged. No alert fires. The HTTP response is still 200 because the endpoint technically "succeeded."

340 failed checkouts. $28,000 in lost revenue. Discovered by accident.

Why traditional monitoring misses this

Sentry — nothing to report. The error was caught and swallowed.

APM dashboards — all green. Response times normal, HTTP status codes normal.

Uptime monitoring — the service is up. It's responding. It's just not doing what it's supposed to do.

This is the gap between "the service is running" and "the service is working correctly." Traditional monitoring watches the first. Almost nobody watches the second.

The fix: log business outcomes, not just errors

The core problem isn't technical — it's philosophical. Most teams log infrastructure events (request received, database query executed, response sent) but not business outcomes (order created, payment processed, event published).

Before: logging the transport

logger.info(`POST /checkout completed in ${ms}ms`)
Enter fullscreen mode Exit fullscreen mode

This tells you the endpoint responded. It doesn't tell you whether the payment actually went through.

After: logging the outcome

logger.info('Payment event published', {
  orderId,
  userId,
  amount,
  stripePaymentId: response.id,
  eventType: 'payment.completed'
})
// or
logger.error('Payment event failed', {
  orderId,
  userId,
  error: err.message,
  stage: 'serialization'
})
Enter fullscreen mode Exit fullscreen mode

Now there's a searchable record of what actually happened. Filter level:error AND service:payment-service and the 340 failures show up immediately — with the exact error, the affected orders, and the timestamp when it started.

The five rules that prevent $28K bugs

1. Every catch block logs the error

No exceptions. No // TODO. If something is caught, it's logged with full context:

catch (err) {
  logger.error('Event serialization failed', {
    orderId,
    error: err.message,
    stack: err.stack,
    input: sanitize(payload)
  })
  throw err
}
Enter fullscreen mode Exit fullscreen mode

2. Logs are structured JSON, not strings

// Useless at scale
console.log(`Payment failed for user ${userId}`)

// Queryable, filterable, alertable
logger.error('Payment failed', { userId, orderId, provider: 'stripe', errorCode: 'serialization_error' })
Enter fullscreen mode Exit fullscreen mode

Template strings can't be filtered. JSON fields can.

3. Every log includes business context

userId, orderId, amount — not just "something failed." When the alert fires at 2am, the difference between "500 errors in the last hour" and "500 payment failures affecting 340 unique users totaling $28,000" determines how fast the team responds.

4. Alert on error rate, not individual errors

Individual error alerts are noisy. Error rate alerts catch gradual degradation:

  • Normal: 0.3% error rate (card declines)
  • After the bug: 12% error rate
  • Alert threshold: 2%

A rate-based alert would have caught this on day one, not day fourteen.

5. Don't trust HTTP status codes

The endpoint returned 200. The business logic failed. These are not contradictory statements — they're just measured at different layers.

Log the business layer. Alert on the business layer. HTTP status codes are necessary but not sufficient.

The tooling question

Structured logging needs somewhere to go. The options, roughly:

  • grep on the server — works until there are multiple servers. Or until someone needs to search at 2am without SSH access.
  • ELK self-hosted — powerful, but maintaining Elasticsearch becomes a second job.
  • Enterprise platforms (Datadog, New Relic) — built for teams with APM, infrastructure monitoring, and six-figure budgets. Overkill if the need is just log search + alerts.
  • Lightweight SaaS — tools like LogFlow, Logtail, or Axiom that focus on log management without the full observability suite. Typically $19-49/month instead of $300+.

The tool matters less than the practice. Pick something that supports structured search and error rate alerts, and the specific vendor is a secondary decision.

The checklist

Before the next deploy, audit these:

  • [ ] Every catch block logs the error with context
  • [ ] Logs are structured JSON with queryable fields
  • [ ] Business outcomes are logged (not just HTTP status)
  • [ ] Logs go somewhere searchable (not just stdout)
  • [ ] At least one error rate alert exists per critical service
  • [ ] The alerting pipeline has been tested end-to-end

The 30 minutes this takes will prevent the 6-hour debugging session. And the $28,000 invoice.


📦 Try LogFlow free — 1 GB/month, no credit card, 5-minute setup: getlogflow.com


What's the most expensive silent bug you've encountered? The catch block? The missing log? The alert that wasn't configured?

Top comments (0)