Python
Exception Handling
Error Handling
Software Development
Programming

Catching all exceptions in Python

Master System Design with Codemia

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

Introduction

Python makes it easy to catch errors broadly, but broad exception handling is only safe when you define the boundary carefully. In most real applications, the goal is not to swallow everything. The goal is to log failures, release resources, and either recover in a controlled way or stop with a clear error path.

Catch Exception, Not Everything

When developers say "catch all exceptions," they usually mean "catch all ordinary runtime failures." In Python, that usually maps to Exception, not a bare except: block.

Exception covers most failures you expect in application code, including ValueError, TypeError, OSError, and many library exceptions. A bare except: also catches KeyboardInterrupt, SystemExit, and GeneratorExit, which are often supposed to escape so the program can stop cleanly.

python
1def parse_port(raw: str) -> int | None:
2    try:
3        return int(raw)
4    except Exception as exc:
5        print(f"invalid port value: {exc}")
6        return None
7
8
9print(parse_port("8080"))
10print(parse_port("abc"))

This is broad, but it is still reasonable because the function returns a clear fallback and does not hide process-level signals.

Use Broad Catches at Application Boundaries

The best place for a broad except Exception block is near the edge of your program. That might be the entry point for a CLI tool, a background worker loop, or a task runner that needs to keep processing independent jobs.

python
1import logging
2
3logging.basicConfig(level=logging.INFO)
4
5
6def process_job(job_id: int) -> None:
7    if job_id == 2:
8        raise RuntimeError("temporary database failure")
9    logging.info("processed job %s", job_id)
10
11
12def main() -> int:
13    for job_id in [1, 2, 3]:
14        try:
15            process_job(job_id)
16        except Exception:
17            logging.exception("job %s failed", job_id)
18
19    return 0
20
21
22if __name__ == "__main__":
23    raise SystemExit(main())

Here the broad catch is deliberate. One bad job does not kill the whole worker, and the log keeps the original stack trace.

Put Specific Exceptions First

If you know how to recover from a particular failure, catch that exception before the general fallback. This keeps recovery logic explicit and avoids turning every problem into the same generic branch.

python
1from pathlib import Path
2
3
4def read_config(path: str) -> str:
5    try:
6        return Path(path).read_text(encoding="utf-8")
7    except FileNotFoundError:
8        return "mode=default\n"
9    except PermissionError as exc:
10        raise RuntimeError(f"cannot read config: {path}") from exc
11    except Exception as exc:
12        raise RuntimeError(f"unexpected config error for {path}") from exc

This pattern communicates intent. Missing files get a default, permission problems become domain-specific errors, and everything else still fails loudly.

Use finally for Cleanup

Broad exception handling often exists because work must be cleaned up even when something breaks. That is what finally is for. Cleanup belongs there instead of being repeated in multiple except branches.

python
1def write_report(path: str, content: str) -> None:
2    file_handle = open(path, "w", encoding="utf-8")
3    try:
4        file_handle.write(content)
5        if "FAIL" in content:
6            raise ValueError("report content rejected")
7    except Exception as exc:
8        print(f"report write failed: {exc}")
9        raise
10    finally:
11        file_handle.close()

If the write fails, the file still closes. The exception is logged and then re-raised so the caller can decide whether the program should continue.

Avoid Silent Failure

The most dangerous pattern is catching broadly and doing nothing. That hides bugs, removes stack traces, and produces systems that appear to work while corrupting state or skipping work.

Bad pattern:

python
1try:
2    run_task()
3except Exception:
4    pass

If the code really can continue, log the failure and return a documented fallback. If it cannot continue, re-raise after logging.

When BaseException Is Appropriate

Catching BaseException is rare, but it is not always wrong. Some frameworks or cleanup routines use it when they must intercept every exit path temporarily before re-raising. That is a narrow, deliberate use case. In ordinary business logic, it is too broad.

python
1def guarded_main() -> int:
2    try:
3        return 0
4    except BaseException:
5        print("shutting down")
6        raise

This form only makes sense when you immediately re-raise and truly want to include interrupts and exit signals in the same cleanup path.

Common Pitfalls

  • Using a bare except: when except Exception: is the safer intent.
  • Swallowing exceptions without logging, returning, or re-raising.
  • Catching broad exceptions deep inside library code where callers lose useful context.
  • Forgetting to put targeted exception handlers before the general fallback.
  • Logging an error message but discarding the original stack trace.

Summary

  • In normal Python code, broad handling usually means except Exception:, not bare except:.
  • Broad catches belong at application boundaries where you can log and recover safely.
  • Put specific exception handlers before the general handler.
  • Use finally for cleanup and re-raise when the caller still needs to know about the failure.
  • Avoid silent failure paths that hide bugs and make behavior unpredictable.

Course illustration
Course illustration

All Rights Reserved.