Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Python Try Except: Error Handling That Survives Production

Sean

Platform Writer

Jul 16, 2026
8 min read

Python’s try/except runs a block of code and hands control to an except clause when a matching exception is raised: put the risky work in try, name the specific exception in except, and put cleanup that must always run in finally. The syntax takes about a minute to learn. What takes longer - and what actually matters once your code is running on a server you pay for - is knowing which errors to catch and which to let kill the process. A bare except that swallows everything is not error handling; it is a way to convert a loud crash into a silent wrong answer, and silent wrong answers are the expensive kind.

Python Try Except: Error Handling That Survives Production

Every tutorial shows you the syntax. Almost none of them tell you that the most common try/except bug is not a syntax mistake - it is catching too much, too broadly, and hiding the one traceback that would have told you what was wrong.

Table of contents

The syntax, quickly

There are four clauses, and you will use two of them most of the time.

try:
    response = requests.get(url, timeout=5)
    data = response.json()
except requests.Timeout:
    logger.warning("Upstream timed out: %s", url)
    data = None
except ValueError:
    logger.error("Upstream returned invalid JSON: %s", url)
    data = None
else:
    logger.info("Fetched %s cleanly", url)
finally:
    session.close()

The parts, in plain terms:

  • try - the code that might raise.
  • except SomeError - what to do when that specific error is raised.
  • else - runs only if no exception was raised. Useful for keeping the try block small.
  • finally - runs no matter what, exception or not. This is where cleanup goes.

The else clause is the one people skip, and it is the one that keeps your try block honest. Anything that cannot raise the error you are catching belongs in else, not in try.

Catch the exception you expect, not every exception

This is the whole game. A bare except: or a broad except Exception: catches everything - including the KeyError from your own typo, the MemoryError that means the box is out of RAM, and the bug you introduced last Tuesday.

# Don't do this.
try:
    user = db.get_user(user_id)
    send_welcome_email(user.email)
except Exception:
    pass

What does this code do when db.get_user returns None because of an unrelated bug? It silently sends nothing, logs nothing, and reports success. You will find out weeks later when someone asks why they never got an email. The traceback that would have told you in seconds was caught and thrown away.

The fix is to name the error you actually expect and have a plan for:

try:
    send_welcome_email(user.email)
except SMTPException as exc:
    logger.exception("Welcome email failed for user %s", user.id)
    enqueue_retry(user.id)

Now an SMTP failure is handled and retried, and an AttributeError from a bug still crashes loudly - which is exactly what you want, because a crash is a message and silence is not.

When a bare except is defensible

There is one honest use: a top-level boundary that must not die, where you catch everything, log it with the full traceback, and re-raise or continue deliberately.

while True:
    job = queue.pop()
    try:
        handle(job)
    except Exception:
        logger.exception("Job %s failed", job.id)
        queue.dead_letter(job)

A worker loop should not exit because one job had a bad payload. The difference between this and the bad example is that nothing is hidden - logger.exception records the full traceback, and the job goes somewhere you can find it. Catch broadly at the edges, narrowly everywhere else.

Note logger.exception rather than logger.error. It includes the traceback automatically. If you take one habit from this article, take that one.

The mistakes that cost real time

These are the ones worth internalising, roughly in order of how much debugging time they waste:

  1. except: pass - the single most expensive line in Python. It converts a crash into a wrong answer.
  2. Catching around too much code. If your try wraps thirty lines, you no longer know which one raised. Wrap the one call that can fail.
  3. Losing the original error. Raising a new exception inside an except block hides the cause unless you use raise NewError(...) from exc.
  4. Using exceptions for ordinary control flow. If a key might be missing, dict.get() is clearer than catching KeyError.
  5. Catching Exception when you meant a specific one. Exception includes bugs. Bugs should crash.

The from exc chaining is worth a moment. Without it, your logs show a ServiceError and no hint of the ConnectionRefused underneath it. With it, you get both halves of the story.

What this looks like on a deployed service

Locally, an unhandled exception prints a traceback and you fix it. On a server, that traceback goes wherever your logs go - and if nobody is reading them, an exception is functionally invisible.

So error handling in production is really two decisions:

  • Which failures are expected? A network timeout, a rate limit, a malformed upload. Catch these, handle them, and keep serving.
  • Which failures are bugs? A TypeError, an AttributeError, a missing config key. Let these crash. A process that restarts on a bug is healthier than one that limps on with corrupt state.

If your service is running under a process manager or a container orchestrator, a crash is not a catastrophe - the process restarts. What you lose by catching everything is not uptime, it is information.

The corollary: whatever you catch, log it with enough context to act on. An entry that says error occurred is barely better than the silence it replaced.

Timeouts are the exception you forgot to catch

Most production Python failures involve waiting for something that never came back. A request to an upstream API, a database query, a lock. If you do not set a timeout, there is no exception to catch - the code just hangs, and a hung worker is worse than a crashed one because nothing restarts it.

# No timeout: this can block forever.
requests.get(url)

# Timeout: now failure is an event you can handle.
requests.get(url, timeout=5)

Set the timeout first, then catch it. try/except cannot save you from a call that never returns.

How this fits the rest of the stack

Error handling decides how much you learn when things break, and how much compute you burn while they do. A worker that retries forever on an exception it should have surfaced is a worker you are paying for by the hour. Before you commit to a shape for the service, the RunxBuild hosting calculator shows the line items together - the API, the worker, the database, the bandwidth - so the cost of that retry loop is a number rather than a surprise. The RunxBuild dashboard is where the team watches what actually happens once it is running.

Useful related references:

FAQ

When should I use try/except instead of an if statement?

Use an if when you can cheaply check the condition first, and try/except when the check is unreliable or the failure is genuinely exceptional. Checking whether a file exists before opening it is a race - it can be deleted between the check and the open - so catching FileNotFoundError is more correct. Checking whether a dict has a key is cheap and clear, so dict.get() beats catching KeyError.

Is a bare except ever acceptable in Python?

Only at a top-level boundary - a worker loop, a request handler, a scheduler tick - where the process must keep running and you log the full traceback with logger.exception before continuing. Everywhere else it hides bugs. Even at a boundary, prefer except Exception: over a bare except:, because a bare except also catches KeyboardInterrupt and SystemExit, which you almost never want.

What is the else clause in try/except for?

It runs only when no exception was raised. Its real value is keeping the try block small: code that cannot raise the error you are catching goes in else, so your except clause cannot accidentally catch something from an unrelated line. It is optional and rarely necessary, but it makes intent clear when the try block would otherwise grow.

Does try/except slow down Python?

Setting up a try block is essentially free in modern Python - the cost is paid only when an exception is actually raised, and raising is comparatively expensive. So try/except is cheap for genuinely rare failures and a poor choice for control flow that triggers on every iteration. If you are catching an exception thousands of times a second, restructure the code.

How do I keep the original error when re-raising?

Use raise NewError("context") from exc. This sets the __cause__ attribute so the traceback shows both your error and the underlying one that triggered it. Without from exc, the original cause is dropped and your logs show a symptom with no source, which is usually the difference between a five-minute fix and an afternoon.

#python try except#python#error handling#exceptions#dev-infra