Skip to main content

Enriches exceptions with plain-English causal chains at throw time

Project description

because

Stack traces show symptoms. because shows causes.


The problem

Your monitoring dashboard lights up with ConnectionError: connection refused on localhost:5432. The stack trace points to db/pool.py:142. You have no idea why.

The real story: a recent deploy added a synchronous DB call to a hot path. Under load, 48 of 50 connections filled up. Three TimeoutErrors were silently swallowed over the prior 10 seconds. By the time the ConnectionError fired, all the context was gone.

This is the ABS warning light telling you the battery is dead. Today's error tooling stops at the symptom.

because captures the context before the crash and attaches it to the exception at throw time.


What it looks like

Without because:

ConnectionError: connection refused on localhost:5432
  File "db/pool.py", line 142, in execute

With because:

ConnectionError: connection refused on localhost:5432
  File "db/pool.py", line 142, in execute

[because context]
  Likely cause: Database connection pool may be exhausted
    • Exception message contains 'QueuePool limit'
    • 8 DB queries in context window (pool was active)
    • 5/8 recent DB queries failed (63% failure rate)
  Recent operations (8):
    [ok] db_query          2.1ms  SELECT * FROM orders WHERE user_id = ?
    [ok] db_query          1.8ms  SELECT * FROM orders WHERE user_id = ?
    [ok] db_query          2.4ms  SELECT * FROM orders WHERE user_id = ?
    [FAIL] db_query        0.5ms  SELECT * FROM orders WHERE user_id = ?  error=OperationalError
    [FAIL] db_query        0.5ms  SELECT * FROM orders WHERE user_id = ?  error=OperationalError
    [FAIL] db_query        0.5ms  SELECT * FROM orders WHERE user_id = ?  error=OperationalError
    [FAIL] db_query        0.5ms  SELECT * FROM orders WHERE user_id = ?  error=OperationalError
    [FAIL] db_query        0.5ms  SELECT * FROM orders WHERE user_id = ?  error=OperationalError

Install

pip install because

With instrumentation extras:

pip install "because[sqlalchemy]"
pip install "because[sqlalchemy,requests]"

Zero-config setup

import because
because.install()

That's it. because hooks sys.excepthook and starts recording operations in the background. Any uncaught exception automatically gets enriched context appended to stderr — no changes to your exception handlers required.


Instrumenting libraries

because ships with instruments for common libraries. Attach them to your existing clients:

from because.instruments.sqlalchemy import instrument as instrument_sa
from because.instruments.requests import instrument as instrument_requests
import requests

instrument_sa(engine)          # your SQLAlchemy engine
instrument_requests(requests.Session())

Each instrument records operation timing, success/failure, and relevant metadata into a per-thread/per-task ring buffer. Zero I/O on the hot path.


Recording swallowed exceptions

Caught-and-not-reraised exceptions are often the real cause of a downstream crash. Wrap risky calls with because.catch() to make them visible:

def get_user(user_id):
    with because.catch(Exception):
        return db.query(User).filter_by(id=user_id).one()
    return None  # reached only if exception was swallowed

When a downstream AttributeError: 'NoneType' object has no attribute 'email' fires, because will surface the swallowed DB error as the likely cause.


Enriching caught exceptions manually

For exceptions you handle yourself:

try:
    process_order(order_id)
except Exception as exc:
    because.enrich_with_swallowed(exc)
    logger.error("Order processing failed", extra={"context": exc.__context_chain__})
    raise

__context_chain__ serializes cleanly into Sentry extra, Datadog error attributes, or structured log fields.


Heuristic patterns (v0)

because ships with a starter set of deterministic cascade patterns:

Pattern Fires when
pool_exhaustion Connection/pool error + recent DB activity or explicit pool message
silent_failure Swallowed exception preceded the current error

Each pattern is a small, independently testable unit. Output always uses hedged language ("likely cause", "contributing factor") — because never claims certainty.


Design principles

  • Honest framing. Output uses "likely cause" and "contributing factor." Wrong-but-confident destroys trust.
  • Zero-config default. import because; because.install() does something useful immediately.
  • No hot-path cost. Instrumentation is bounded ring buffers. Enrichment runs only on exception.
  • Composable with existing tools. Attaches to Sentry, Datadog, and structured logging — doesn't replace them.
  • Library, not platform. Pure Python, no required backend, ships as a pip package.

Examples

Runnable demos in examples/:

python examples/pool_exhaustion.py   # connection pool saturated under load
python examples/silent_failure.py    # swallowed DB error causes downstream crash

Each demo shows the cascade being triggered and because surfacing the cause.


Roadmap

  • v0.2 — Optional LLM-based explanation (async, deferred, BYO API key)
  • v1.0 — Cross-process / cross-service causal reasoning
  • More instruments: httpx, redis-py, logging, stdlib socket
  • Framework middleware: Flask, FastAPI, Django
  • Sentry / Datadog / OpenTelemetry integration helpers

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

because_py-0.2.0.tar.gz (33.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

because_py-0.2.0-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file because_py-0.2.0.tar.gz.

File metadata

  • Download URL: because_py-0.2.0.tar.gz
  • Upload date:
  • Size: 33.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.16.5 cpython/3.13.3 HTTPX/0.28.1

File hashes

Hashes for because_py-0.2.0.tar.gz
Algorithm Hash digest
SHA256 296e68653880a53711b4d1ce89cf3e7167693840c68b570f6fc785ed851b7412
MD5 dc0a4f0844e1e6da27a53adb1b9ae25e
BLAKE2b-256 bb933c5ea9bd9e00141ae911eefb005551a096d019027ab9c8a6a5c3ae208c8d

See more details on using hashes here.

File details

Details for the file because_py-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: because_py-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 25.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.16.5 cpython/3.13.3 HTTPX/0.28.1

File hashes

Hashes for because_py-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1131f0bbb8b3c6b66eec36f185c0b75eba7fdfc09e5e854e6e84e2cac375b568
MD5 3006147827ffd9b7d74076ca51b73864
BLAKE2b-256 4805907445005af9a82bf3219f083cf4fec740270cb7d1cbe3fb9a12b70c9eef

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page