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.1.0.tar.gz (26.1 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.1.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: because_py-0.1.0.tar.gz
  • Upload date:
  • Size: 26.1 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.1.0.tar.gz
Algorithm Hash digest
SHA256 71015cef00f9a71cff6c8c7bcb9c1e1503ec86a98cf1fcd44b653744bb1cc13a
MD5 b7497a993fb4a932ebd6949266e0c467
BLAKE2b-256 38474ac6799110db359d7814b0c48aa0b966989a822b9c15d58957f7e3cc732c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: because_py-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.6 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8c18d7660ed1a6c8c276a482bb42c4bcc56ae330565f9ef92dcc687f8e9bfdb7
MD5 d3c6401a873eca9fc6149d7bb3b94706
BLAKE2b-256 c149ff74cd95965a3fc557c6d867486ca5400758cfb76ad722334a984fff64ec

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