{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Handle multiple exceptions\n",
    "\n",
    "**The question.** A block of code can fail in several ways — a missing file, malformed JSON, a missing field in the parsed data — and each failure needs its own response. You want to write that without either a huge `except Exception` catch-all or a deeply-nested pile of `try`/`except` blocks.\n",
    "\n",
    "The answer: one `try` block with one `except` clause per failure type, ordered specific-to-general. Python evaluates them top-to-bottom and executes the first one that matches.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# One try, several excepts — each failure mode gets its own response.\n",
    "# Specific types first; a broad fallback last if you need a safety net.\n",
    "import json\n",
    "\n",
    "\n",
    "def parse_json_field(raw: str, field: str) -> str | None:\n",
    "    '''Parse a JSON string, extract the named field, or return None on any failure.'''\n",
    "    try:\n",
    "        data = json.loads(raw)\n",
    "        return str(data[field])\n",
    "    except json.JSONDecodeError as exc:\n",
    "        # Malformed input — surface the position, it's the most useful debugging clue.\n",
    "        print(f'Invalid JSON at position {exc.pos}: {exc.msg}')\n",
    "    except KeyError:\n",
    "        # Parsed fine, but field isn't there.\n",
    "        print(f'Field {field!r} not found in parsed data')\n",
    "    except TypeError:\n",
    "        # Parsed value wasn't a dict — can't index it.\n",
    "        print('Parsed data is not a dict; cannot look up fields')\n",
    "    return None\n",
    "\n",
    "\n",
    "print(parse_json_field('{\"name\": \"Alice\"}', 'name'))      # → 'Alice'\n",
    "print(parse_json_field('not json', 'name'))                 # → JSONDecodeError\n",
    "print(parse_json_field('{\"name\": \"Alice\"}', 'age'))       # → KeyError\n",
    "print(parse_json_field('[1, 2, 3]', 'name'))                # → TypeError\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: group exception types that share handling\n",
    "\n",
    "When two failures deserve identical handling — say, a missing file and a permissions problem both become 'can't read config' — group them in a tuple. The tuple form keeps the code short; the `as exc` bind still gives you the actual raised type if you want to inspect it.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def safe_read(path):\n",
    "    try:\n",
    "        with open(path, encoding='utf-8') as f:\n",
    "            return f.read()\n",
    "    except (FileNotFoundError, PermissionError) as exc:\n",
    "        # Both are 'we can't read this file' — identical response.\n",
    "        print(f'cannot read {path}: {type(exc).__name__}: {exc}')\n",
    "        return None\n",
    "\n",
    "\n",
    "print(safe_read('/definitely/not/a/file'))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: bare `raise` to log and propagate\n",
    "\n",
    "When you want to record that an exception happened but still let the caller handle it, bare `raise` (no arguments) re-raises the current exception with its original traceback intact. Do *not* write `raise exc` — that resets the traceback and loses the original call chain.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "logger = logging.getLogger('demo')\n",
    "\n",
    "def process_age(raw):\n",
    "    try:\n",
    "        age = int(raw)\n",
    "    except ValueError:\n",
    "        logger.warning('failed to parse age from %r', raw)\n",
    "        raise                    # same exception, original traceback\n",
    "    if age < 0:\n",
    "        raise ValueError(f'age must not be negative, got {age}')\n",
    "    return age\n",
    "\n",
    "try:\n",
    "    process_age('abc')\n",
    "except ValueError as exc:\n",
    "    print(f'caller received: {exc}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: chain exceptions with `raise ... from ...`\n",
    "\n",
    "When you replace a low-level exception with a domain-specific one, use `from exc` so the original cause is preserved on the new exception's `__cause__`. Tracebacks show both, connected by 'The above exception was the direct cause of the following exception' — which is usually what you want.\n",
    "\n",
    "Use `from None` to *suppress* the original when it's an implementation detail the caller shouldn't care about.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ConfigurationError(Exception):\n",
    "    '''Raised when configuration is missing or invalid.'''\n",
    "\n",
    "\n",
    "def load_port(config):\n",
    "    try:\n",
    "        raw = config['port']\n",
    "    except KeyError as exc:\n",
    "        raise ConfigurationError(\"'port' is missing from config\") from exc\n",
    "    try:\n",
    "        return int(raw)\n",
    "    except ValueError as exc:\n",
    "        raise ConfigurationError(f\"'port' must be an integer, got {raw!r}\") from exc\n",
    "\n",
    "\n",
    "try:\n",
    "    load_port({})\n",
    "except ConfigurationError as exc:\n",
    "    print(f'ConfigurationError: {exc}')\n",
    "    print(f'caused by: {exc.__cause__!r}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why this works\n",
    "\n",
    "Python checks `except` clauses in the order they're written and takes the first whose type matches the raised exception (via `isinstance`). That's why order matters: a general `except Exception` before a specific `except ValueError` would capture the `ValueError` first and the specific clause would never run.\n",
    "\n",
    "Each branch does the single most useful thing for that failure — surface the JSON position, or name the missing field, or explain the shape mismatch. That's the shape of 'helpful error handling': the diagnostic message names the proximate cause and points the caller at the fix.\n",
    "\n",
    "If two or more failure types deserve identical handling, group them in a tuple: `except (KeyError, TypeError) as exc:`. That's the grouping version — see the extra cells.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "Keep `try` blocks small. Each line inside a `try` is a potential source of exceptions; a five-line try around 'do everything' makes it hard to tell which call raised. Put only the line(s) that might raise the exception you're catching inside the try — the [common-mistakes recipe](https://agilearn.co.uk/guides/error-handling/recipes/avoid-common-mistakes) has the 'try block too big' anti-pattern in detail.\n",
    "\n",
    "When you need to wrap a low-level exception in a more meaningful one (`KeyError` → `ConfigurationError`), use `raise NewError(...) from exc` — that preserves the original as `__cause__`. See the extra cells for the chaining pattern.\n",
    "\n",
    "A final `except Exception` clause is a safety net for the truly unexpected. Use it sparingly — it can mask bugs, and it should always log what it caught so you can investigate.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Create custom exceptions](https://agilearn.co.uk/guides/error-handling/recipes/create-custom-exceptions) — when to raise your own types from an `except` block.\n",
    "- [Avoid common error handling mistakes](https://agilearn.co.uk/guides/error-handling/recipes/avoid-common-mistakes) — over-broad catches, silent swallow, the big-try anti-pattern.\n",
    "- [try/except syntax reference](https://agilearn.co.uk/guides/error-handling/reference/try-except-syntax-reference) — the full grammar and the `else`/`finally` clauses.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbformat_minor": 5,
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}