exception handling
nested try/except
python programming
error management
re-raising exceptions

How to re-raise an exception in nested try/except blocks?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Nested try and except blocks are useful when low level failures must be translated into business level errors. The important part is how you re raise exceptions, because traceback quality determines how quickly you can debug production incidents. In Python, different raise styles change error history, so choosing the right one is critical.

Re Raise Styles and What They Mean

Python gives you three common patterns:

  • raise inside an except block: re throws the current exception with original traceback.
  • raise NewError(...) from exc: wraps with domain context and keeps the causal chain.
  • raise NewError(...) from None: hides original cause from output, usually for user facing boundaries.

Preserve Original Traceback with Bare raise

Use bare raise when your current layer cannot add useful context.

python
1def read_file(path: str) -> str:
2    try:
3        with open(path, "r", encoding="utf-8") as fh:
4            return fh.read()
5    except OSError:
6        # Preserve the exact original stack trace
7        raise

Avoid raise exc here. That rethrows but may alter traceback details and confuse debugging.

Add Domain Context with from exc

If your layer can explain business meaning, wrap the low level exception.

python
1class ConfigLoadError(Exception):
2    pass
3
4
5def load_config(path: str) -> dict:
6    try:
7        text = read_file(path)
8        return parse_config(text)
9    except Exception as exc:
10        raise ConfigLoadError(f"Cannot load config at {path}") from exc
11
12
13def parse_config(text: str) -> dict:
14    import json
15    return json.loads(text)

Now callers see ConfigLoadError, and the original JSONDecodeError or OSError remains attached in __cause__.

Suppress Internal Details at API Boundary

At a public boundary you may not want to expose internals. Use from None carefully.

python
1class PublicValidationError(Exception):
2    pass
3
4
5def validate_user_payload(payload: dict) -> None:
6    try:
7        age = payload["age"]
8        if age < 0:
9            raise ValueError("age must be non-negative")
10    except Exception:
11        # Keep user-facing error simple
12        raise PublicValidationError("Invalid request") from None

Do this only where security or user experience requires hiding internal stack details.

Nested try and except Example

This example shows a realistic multi layer flow.

python
1class RepositoryError(Exception):
2    pass
3
4class ServiceError(Exception):
5    pass
6
7
8def save_to_db(record: dict) -> None:
9    try:
10        if "id" not in record:
11            raise KeyError("id missing")
12        # Simulate storage write
13        if record["id"] == 0:
14            raise RuntimeError("connection timeout")
15    except Exception as exc:
16        raise RepositoryError("Database write failed") from exc
17
18
19def create_user(record: dict) -> None:
20    try:
21        save_to_db(record)
22    except RepositoryError as exc:
23        raise ServiceError("User creation failed") from exc
24
25
26if __name__ == "__main__":
27    try:
28        create_user({"id": 0, "name": "Ava"})
29    except ServiceError as err:
30        print("Top-level error:", err)
31        print("Cause level 1:", type(err.__cause__).__name__)
32        print("Cause level 2:", type(err.__cause__.__cause__).__name__)

The chain shows service layer context plus repository context plus root cause.

Logging Without Duplicates

A common issue in nested handling is duplicate logs. If each layer logs at error level and re raises, one failure can generate noisy repeated messages. A better model:

  • Intermediate layers add context through exception chaining.
  • One boundary layer logs final failure with full traceback.
python
1import logging
2
3logger = logging.getLogger(__name__)
4
5
6def handle_request(payload: dict) -> None:
7    try:
8        create_user(payload)
9    except ServiceError:
10        logger.exception("Request failed")
11        raise

logger.exception already includes stack trace, so avoid logging the same traceback elsewhere.

Common Pitfalls

  • Using raise exc instead of bare raise when preserving traceback.
  • Catching broad Exception too early and hiding specific failure handling.
  • Wrapping exceptions without from exc, which breaks causal chain visibility.
  • Logging at every layer and producing duplicate noisy logs.
  • Using from None in internal services where root cause visibility is needed.

Summary

  • Use bare raise to keep original traceback unchanged.
  • Use raise NewError(...) from exc to add domain context safely.
  • Use from None only at boundaries where internal details must be hidden.
  • Keep nested handlers purposeful and avoid broad catches without intent.
  • Log failures once at a clear boundary to preserve signal quality.

Course illustration
Course illustration

All Rights Reserved.