ScreenshotNeo

BlogGuides

Exception Handling in Programming: A Practical Guide

Learn how exceptions are raised, caught, propagated, and cleaned up—and when explicit error returns are a better fit.

By the ScreenshotNeo team4 October 20268 min read

Exception handling lets a program respond to failures that interrupt normal control flow. Put potentially failing work in a protected region, catch only failures the current layer can handle, and use cleanup logic to release resources as control leaves that region. If the current code cannot restore a known state, let the failure propagate to a caller that can. Not every language uses exceptions for ordinary errors: Go and Rust commonly represent recoverable errors as returned values.

What exception handling does

An exception is a signal that an operation failed or that normal execution cannot continue as written. Depending on the language and operation, examples include a missing file, invalid input, a failed conversion, or an unavailable service. Exception handling provides a way to separate the operation that may fail from the code that decides how to respond.

A useful mental model has four parts:

  1. Protected work: perform an operation that may fail.
  2. Handler: respond to a specific failure if this layer can do so safely.
  3. Propagation: if no suitable local handler exists, the failure can move outward to a caller.
  4. Cleanup: release resources as control leaves the operation, whether it succeeds or fails.

Handling an exception does not automatically repair the underlying problem. A handler should either restore a valid state, provide a safe alternative, or report the failure and stop that operation. Catching an error and continuing with invalid state can make the eventual failure harder to diagnose.

Python example: catch a specific failure

This runnable example parses a command-line amount. It catches the expected conversion error, prints a clear message, and exits with a nonzero status. It does not catch every possible exception.

import sys


def parse_amount(value: str) -> int:
    try:
        amount = int(value)
    except ValueError as exc:
        raise ValueError("amount must be a whole number") from exc

    if amount < 0:
        raise ValueError("amount must be zero or greater")
    return amount


def main() -> int:
    raw = sys.argv[1] if len(sys.argv) > 1 else "12"
    try:
        amount = parse_amount(raw)
    except ValueError as exc:
        print(f"Invalid input: {exc}", file=sys.stderr)
        return 2

    print(f"Accepted amount: {amount}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Save it as amount.py. Run python amount.py 12 for a successful result, or python amount.py twelve to see the handled error. The lower-level function adds context and re-raises; the command-line boundary turns the known input failure into a user-facing message and exit code. Other programming errors are not silently converted into input errors.

Choosing what to catch

Catch an exception where you have enough information to make a useful decision. That may be close to the operation, such as translating a missing configuration file into a default, or at an application boundary, such as turning a request failure into an error response.

Situation Good response
Invalid user input Explain the expected format and ask for valid input.
Optional resource is absent Use a documented fallback if the application can remain valid.
Temporary dependency failure Retry only when the operation is safe to retry, with a limit and suitable delay.
Unexpected programming defect Allow it to reach a suitable reporting boundary; preserve diagnostic context.
Failure leaves state uncertain Stop the operation or roll it back instead of continuing as if it succeeded.

Prefer handlers for specific exception types over a catch-all. A broad handler can mask programming errors or misclassify a failure. Microsoft advises catching only when the application can be left in a known state, and Python’s tutorial warns that broad handling can hide real errors. See Microsoft’s C# exception guidance and Python’s errors and exceptions tutorial.

How exception propagation works

A handler does not have to sit immediately next to the line that failed. If a function does not handle an exception, callers may get the opportunity to handle it. In C#, the runtime searches outward through the call stack for a matching handler; Python likewise lets an exception pass to an enclosing try statement when the current one does not handle it. The exact rules and terminology differ between languages.

Propagate a failure when the current layer lacks the context or authority to recover. Add useful context when re-raising, but avoid swallowing the original cause. If no handler accepts an exception, it is unhandled: the program or operation may stop, and the runtime or framework reports the failure according to its rules.

Cleanup is separate from error handling

Cleanup releases resources when control exits a block. It does not fix the exception. In languages with a finally construct, that block is intended for cleanup that must run whether the protected work succeeds or fails. Python and C# document this role; JavaScript’s guide uses ensuring a file is not left open as an example. For resources with dedicated management constructs, prefer the language’s idiom, such as Python’s with statement or JavaScript’s resource-specific APIs where supported by the target runtime.

try:
    file = open("input.txt", encoding="utf-8")
    try:
        contents = file.read()
    finally:
        file.close()
except OSError as exc:
    print(f"Could not read input: {exc}")

In ordinary Python code, a context manager expresses this cleanup more directly:

try:
    with open("input.txt", encoding="utf-8") as file:
        contents = file.read()
except OSError as exc:
    print(f"Could not read input: {exc}")

Use cleanup for things such as closing files, releasing locks, and returning connections to a pool. Keep it reliable and limited: an error during cleanup can complicate the original failure.

Exceptions and returned errors across languages

Exception handling is one error-handling design, not a universal requirement. Syntax, selection rules, propagation, cleanup, and the meaning of recoverability vary.

Language Common pattern Key detail
C# try, catch, and finally Handlers match exception types; an unhandled exception can unwind through callers.
Java try, catch, and cleanup constructs Exception categories and compile-time rules affect which failures must be handled or declared.
Python try, except, else, and finally Specific exception classes make handlers clearer; unmatched exceptions can propagate outward.
JavaScript try, catch, and finally Thrown values and asynchronous rejection handling have different control-flow details.
Go Return an error value alongside the result Callers ordinarily inspect returned errors; panic/recover has a narrower role.
Rust Return Result for recoverable failures The caller handles or propagates the value; unrecoverable failures use a different path.

For example, ordinary Go code often checks an error result explicitly, while Rust code commonly uses Result and the ? operator to return an error to its caller. The Go FAQ explains the project’s preference for explicit error returns: “We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code.” See the Go FAQ and Rust Book’s error handling chapter. For Java, see Oracle’s exception summary; for JavaScript, see MDN’s control flow and error handling guide.

Writing reliable handlers

  • Handle at the right boundary. A low-level helper may lack the context to retry or show a useful message; a top-level request handler often has it.
  • Catch narrowly. Handle only the failure modes that have a defined response.
  • Preserve context. Include the operation and relevant safe details in logs or propagated errors. Do not expose secrets or sensitive input to users.
  • Do not retry blindly. Retrying a non-idempotent operation may duplicate work. Bound retries and stop when the failure is permanent.
  • Keep cleanup independent. Close or release resources even when a handler decides to propagate.
  • Make failure visible. Record enough diagnostic information to investigate; do not report success after a failed operation.
  • Test failure paths. Check the handled case, the propagated case, and cleanup behavior, not only the successful path.

Common problems and fixes

Symptom Likely cause Fix
The program stops despite a try block The handler does not match the thrown exception, or the failure happened outside the protected region. Confirm the exception type and scope; handle it at the layer that can respond safely.
A bug appears as a normal result A broad handler swallowed an unexpected exception. Narrow the caught types and preserve or report unexpected failures.
A resource remains open after failure Cleanup was placed only on the success path. Use finally or the language’s resource-management construct.
Retrying creates duplicate actions The operation was not safe to repeat. Retry only operations with safe repeat semantics, or add an idempotency mechanism at the application level.
The message lacks the original cause The exception was replaced without preserving its context. Use the language’s cause/chaining feature when raising a more useful error.
Cleanup failure hides the original failure The cleanup path itself raised an error. Keep cleanup small, and define how cleanup errors are recorded without discarding the primary failure.

Performance, reliability, and cost

Exception handling adds control-flow and maintenance considerations, but this guide’s sources provide no suitable benchmark for comparing its runtime cost with returned errors. Avoid designing around an unsupported performance number. Measure a representative workload if performance is material, and keep exceptions for exceptional control flow rather than using them as routine branching where the language’s conventions favor explicit values.

Reliability depends more on correct boundaries, clear failure contracts, safe recovery, and dependable cleanup than on the presence of a try block. The practical cost of poor handling includes hidden defects, invalid state, lost diagnostic context, and resource leaks. No financial cost applies to the language constructs themselves; operational costs depend on the surrounding system and recovery policy.

Or skip the browser setup

If your work also needs website screenshots, ScreenshotNeo provides a single GET request that returns an image or PDF. This is separate from exception handling, but can remove browser automation setup for screenshot capture. See the ScreenshotNeo website and API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

FAQ

Should every exception be caught?

No. Catch a failure only when the current layer can respond safely or add useful context. Otherwise, let it reach a caller that can make that decision.

Does a finally block handle an exception?

No. It is for cleanup as control leaves a protected region. A handler decides what to do about the failure.

Are Go and Rust exception-free?

Their ordinary error-handling patterns use returned error values, such as Go’s error results and Rust’s Result. Go also has panic and recovery mechanisms, but they are not the usual way to report routine errors.

When should an error stop the program?

Stop the operation when the program cannot restore a valid state or safely continue. A top-level boundary can report the failure and choose an appropriate exit or response.