AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Error Handling

📚 Technology⏱️ 22 min read🎓 Grade 8
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 22 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Two students write the exact same program for the school canteen committee: split a group snack bill evenly among friends. Priya's version works perfectly all week. Arjun's version crashes on day two, throws a wall of red text at a Class 8 student who has no idea what a "traceback" is, and closes itself. Both programs contain the same three lines of arithmetic. The difference is that Priya's program knows what to do when something goes wrong, and Arjun's does not. That difference is the entire subject of this chapter.

A Program That Works — Until Someone Types the Wrong Thing

Here is Arjun's program. It asks how many friends are splitting a canteen bill of Rs. 240, then prints each person's share.

students = int(input("How many friends are splitting the bill? "))
bill = 240
share = bill / students
print("Each person pays Rs.", share)

Run it with a normal answer like 4, and it works: students becomes 4, share becomes 240 / 4 = 60.0, and the program prints "Each person pays Rs. 60.0". Nothing unusual so far.

Now run it on the day nobody has shown up to split the bill yet, and someone types 0. Python executes students = 0, then bill = 240, then reaches share = bill / students — and division by zero has no answer, so Python immediately stops executing the program right there and prints an error message ending in ZeroDivisionError: division by zero. The final print line never runs. Or suppose someone types the word "four" instead of the digit 4. The very first line, int(input(...)), tries to convert the text "four" into a whole number and cannot, so Python halts immediately with ValueError: invalid literal for int() with base 10: 'four'. In both cases, the program does not just fail to give the right answer — it stops dead, and every apps built on top of it (a bigger canteen management system, say) would stop too.

This is the situation that error handling exists to fix — not by preventing mistakes from happening, but by deciding, in code, what the program should do when one happens.

Three Different Kinds of "Something Went Wrong"

Before writing any fix, it helps to separate three things that beginners often lump together as "an error," because each one needs a completely different response.

A syntax error means the code is not even valid Python — a missing colon, an unmatched bracket, a misspelled keyword. Python refuses to run the program at all and reports the problem before a single line executes. No amount of error handling fixes a syntax error; you simply have to correct the code.

A runtime error, also called an exception, means the code is syntactically valid and starts running fine, but hits a situation it cannot continue from — dividing by zero, converting "four" to a number, opening a file that does not exist. This is the category error handling is built for: you cannot always stop a user from typing "four", but you can control what your program does about it.

A logical error means the program runs from start to finish without complaining, and still gives the wrong answer — for example, calculating bill * students instead of bill / students. Python has no way of knowing this is "wrong," because nothing about it is invalid. Error handling cannot catch logical errors either; only careful thinking, testing, and tracing your code by hand can catch those. Keeping these three categories separate matters for the rest of this chapter, because everything from here on deals only with the middle one: runtime exceptions.

The try/except Block

Python lets you mark a block of code as "risky" and attach a plan for what to do if it fails, using try and except. Here is Priya's version of the same canteen program:

try:
    students = int(input("How many friends are splitting the bill? "))
    bill = 240
    share = bill / students
    print("Each person pays Rs.", share)
except ValueError:
    print("Please enter a whole number, not text.")
except ZeroDivisionError:
    print("Cannot split the bill among zero people.")

Read this as an instruction, not just syntax: "Attempt everything inside try. If it all succeeds, skip every except block entirely. If something inside try raises an exception, stop at that exact line, jump straight to whichever except block matches the type of exception that occurred, and run that block instead — skipping the rest of the try block completely."

Trace three separate runs by hand, because tracing is exactly what CBSE practical exams ask you to do with this kind of code:

Run A — input 4: students = 4 succeeds. bill = 240 runs. share = 240 / 4 = 60.0 succeeds. The print line runs, showing "Each person pays Rs. 60.0". No exception occurred anywhere, so both except blocks are skipped entirely — they never even get checked.

Run B — input 0: students = 0 succeeds (0 is a perfectly valid integer). bill = 240 runs. Then share = 240 / 0 raises ZeroDivisionError at that exact line. The final print line inside try is skipped completely — it never runs. Python looks at the exception type, finds it matches except ZeroDivisionError, and runs that block: "Cannot split the bill among zero people."

Run C — input "four": The very first line, int(input(...)), raises ValueError before students is even assigned. Every later line in try — including bill = 240 — is skipped. Python matches the exception to except ValueError and prints "Please enter a whole number, not text."

Same three-line calculation as Arjun's program, same possible mistakes — but instead of a crash and a wall of unreadable text, the user gets a sentence they can act on. This is also, in miniature, why real apps behave the way they do: when a UPI payment app shows "Please check the amount and try again" instead of crashing to the home screen, it is almost certainly running your money-transfer code inside something equivalent to a try/except block.

Common Misconception: "Just Catch Everything, to Be Safe"

A very natural but incorrect idea, once you have seen except ValueError and except ZeroDivisionError, is: "Why bother naming specific exception types? I'll write a bare except: with nothing after it, and it will catch anything at all — then my program can never crash." This is wrong, and it is wrong in a way that actively hides bugs rather than fixing them. Watch what happens when the programmer's own code — not the user's input — contains a mistake:

try:
    students = int(input("How many friends? "))
    bill = 240
    share = bil / students
    print("Each person pays Rs.", share)
except:
    print("Something went wrong.")

Look closely at the fourth line: bil, not bill — a typo in the variable name. This has nothing to do with what the user typed. Even if the user enters a perfectly sensible number like 4, this program will raise a NameError (Python has no variable called bil), and the bare except: will silently swallow it and print "Something went wrong" — the exact same unhelpful message it would print for a genuine bad input. A programmer testing this code would see "Something went wrong," assume the problem is with user input handling, and spend an hour hunting in the wrong place, never suspecting the real bug is a five-character typo sitting in plain sight. Naming the exact exception type you expect — except ValueError, except ZeroDivisionError — means only those specific, anticipated problems get handled quietly. Everything else, including your own coding mistakes, still shows up as a visible crash where you can actually find and fix it. A bare except: is not "safer" — it is a blindfold.

Handling More Than One Way to Fail, Precisely

The canteen program in Run B and Run C already shows the pattern for handling multiple, distinct failure types: write one except clause per exception type, in any order, and Python checks them against whichever exception actually occurred and runs only the matching one. You can also capture the exception object itself, using as, to see exactly what Python has to say about the failure:

try:
    students = int(input("How many friends? "))
    share = 240 / students
except (ValueError, ZeroDivisionError) as e:
    print("Could not split the bill:", e)

Grouping ValueError and ZeroDivisionError together in parentheses tells Python to run this one block for either type. If the input is 0, e holds Python's own ZeroDivisionError object, and str(e) is the text "division by zero", so the program prints "Could not split the bill: division by zero". If the input is "four", e holds a ValueError whose message is invalid literal for int() with base 10: 'four', and that full sentence gets printed after the colon. This is genuinely useful during development — you see Python's real diagnostic message instead of hiding it — while the surrounding sentence still keeps the output friendly for an actual user.

else and finally: Two Blocks With Very Different Jobs

Two more keywords complete the full picture. An else block, if you add one, runs only when the entire try block finished with no exception at all — it is where you put code that should happen only on success, separate from the risky code itself. A finally block, if you add one, runs no matter what happened — exception or no exception, handled or unhandled — always, as the very last thing before the program moves on. This matters for things like closing a file: you want that to happen whether or not something went wrong while the file was open.

try:
    students = int(input("How many friends? "))
    share = 240 / students
except ZeroDivisionError:
    print("Cannot split among zero people.")
else:
    print("Each person pays Rs.", share)
finally:
    print("Bill-splitting attempt finished.")

Trace it for input 4: the try block completes with no exception, so except is skipped, else runs and prints "Each person pays Rs. 60.0", and then finally runs and prints "Bill-splitting attempt finished." Two lines of output. Now trace it for input 0: the try block raises ZeroDivisionError, so except runs and prints "Cannot split among zero people." — but because an exception did occur, else is skipped entirely, even though the except block successfully handled the problem. Then finally still runs and prints "Bill-splitting attempt finished." Also two lines of output, but a different second-to-last line. The key distinction students often miss: else checks whether an exception happened at all, not whether it was handled — a handled exception still counts as "an exception happened," so else is skipped either way.

Where Exception Types Come From: the Exception Hierarchy

Every exception type used above — ValueError, ZeroDivisionError, NameError — is a Python class, and these classes are organized into a family tree. Nearly everything you will ever catch is a descendant of a class called Exception:

BaseException
├── SystemExit, KeyboardInterrupt, GeneratorExit   (rare signals, not everyday errors)
└── Exception                                       (parent of nearly everything below)
     ├── ArithmeticError
     │    └── ZeroDivisionError
     ├── LookupError
     │    ├── IndexError
     │    └── KeyError
     ├── ValueError
     ├── TypeError
     ├── NameError
     └── OSError
          └── FileNotFoundError

This is why except Exception is a much better "catch several kinds of problems" tool than a bare except: — it catches every ordinary runtime error, ArithmeticError down to FileNotFoundError, while deliberately leaving out a few rare, special signals that live directly under BaseException instead of under Exception: things like KeyboardInterrupt, which is what Python raises when a user presses Ctrl+C to force-stop a running program. That separation is intentional, not an oversight — it means that even a program written with a broad except Exception can still be interrupted by the person running it. A bare except:, by contrast, sits at the very top of the tree and catches everything, KeyboardInterrupt included, which is one more concrete reason it is considered poor practice: it can make a program that refuses to stop even when its user wants it to.

Raising Your Own Errors: the raise Statement

So far, every exception has come from Python itself — dividing by zero, converting bad text. You can also decide, in your own code, that a value is unacceptable and manually trigger an exception with raise, even though nothing in Python itself would have complained:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative.")
    return age

try:
    user_age = set_age(-5)
except ValueError as e:
    print("Invalid input:", e)

Trace it: set_age(-5) is called inside the try block. Inside the function, age is -5, the condition age < 0 is true, so Python executes raise ValueError("Age cannot be negative.") — this manually creates and triggers a ValueError, exactly as if Python had detected the problem itself. The return age line never runs. Control jumps out of the function and out of the try block to except ValueError as e, where e now holds the exception with the message you wrote, so the program prints "Invalid input: Age cannot be negative." This is the tool that turns "the code technically works" into "the code refuses to accept nonsense," which is essential once you start writing functions other people (or your future self) will call with unpredictable values.

Following the Whole Flow: try → except → else → finally

The diagram below traces every possible path through a full try/except/else/finally statement, including the two things learners most often get backwards: that finally always runs before the program either continues or crashes — even on the unhandled path — and that a crash does not skip cleanup, it happens right after it.

try: risky_code() Exception raised inside try block? No exception → else: block runs (only on success) Does an except clause match it? Matching except: block runs No except clause matches — stays unhandled (so far) No Yes Match No match finally: always runs (e.g. closing a file, cleanup code) Program continues normally after the try/except statement finally already ran above. The exception now keeps propagating — program stops with a traceback

Read the green path and the red path as two separate journeys that both pass through the same finally box before they end. The green path (no exception, or an exception that was successfully handled) flows out of finally into "program continues normally." The red path (no except clause matched) flows out of the very same finally box into "program stops with a traceback." The crash never bypasses cleanup — Python guarantees finally runs before it decides what happens next, whether that next step is quietly continuing or propagating the error further up the program.

Practice: Check Your Understanding

1. Trace this by hand and predict exactly what gets printed, in order.

try:
    x = int("12")
    y = 10 / (x - 12)
    print("Result:", y)
except ZeroDivisionError:
    print("Cannot divide by zero.")
finally:
    print("Done.")

Answer: x becomes 12. Then x - 12 is 0, so 10 / 0 raises ZeroDivisionError before the print("Result:", y) line is ever reached. The matching except runs, printing "Cannot divide by zero.", and then finally runs, printing "Done." — two lines total, and "Result:" never appears.

2. A classmate's program uses a bare except: and it keeps printing "Something went wrong" even on inputs that should clearly work. What is the most likely explanation, and why does the bare except: make it harder to find?

Answer: The most likely explanation is a bug in the programmer's own code — a typo in a variable name, a wrong operator, an off-by-one mistake — that has nothing to do with the input at all. A bare except: catches every exception type without distinction, including ones caused by genuine coding mistakes, so it prints the same generic message for a bad typo as it would for bad user input. Naming specific exception types instead would let the typo's real error (typically a NameError) show up as a visible crash, which is far easier to diagnose than a vague, swallowed message.

3. If you write except Exception as e: to catch every mistake you can think of, will pressing Ctrl+C still be able to stop the program? Explain using the exception hierarchy.

Answer: Yes, it will still stop the program. Pressing Ctrl+C raises KeyboardInterrupt, which inherits directly from BaseException, not from Exception. Since except Exception only matches Exception and its descendants, KeyboardInterrupt is not caught by it, and Ctrl+C still works as expected. This is by design, so that no ordinary error-handling code can accidentally trap a user who is trying to force-stop a runaway program.

4. In a try/except/else/finally statement, if the try block raises an exception that an except clause successfully handles, does the else block still run?

Answer: No. else only runs when the try block completes from start to finish with no exception at all. Whether or not the exception was successfully handled afterward makes no difference — the moment any exception is raised inside try, else is skipped. finally, on the other hand, still runs regardless.

5. Write a try/except block that safely converts user input into a whole-number age, printing "Please enter digits only." if the input is not a valid number.

try:
    age = int(input("Enter your age: "))
    print("You are", age, "years old.")
except ValueError:
    print("Please enter digits only.")

Summary

An error you have not planned for stops your program cold; the same error, anticipated with a try/except block, becomes a message a user can actually act on. Keep syntax errors, runtime exceptions, and logical errors separate in your mind — only runtime exceptions are what try/except is built for. Prefer naming specific exception types (ValueError, ZeroDivisionError, and the rest) over a bare except:, because specificity handles the failures you expected while still letting your own coding mistakes surface visibly instead of hiding behind a generic message. Add else when you have code that should run only after a fully successful try, and add finally when you have cleanup that must happen no matter what — including right before an unhandled exception finishes crashing the program, never after. Use raise to trigger your own exceptions when a value is unacceptable by the rules of your program, even if Python itself would have been perfectly happy to accept it. Every exception type descends from Exception, which itself descends from BaseException alongside a few rare, special signals like KeyboardInterrupt — understanding that family tree is what tells you exactly what a given except clause will, and will not, catch.

Think About It

Think about this: How would you explain error handling to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

← Event-DrivenMemory Management: How Computers Remember →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn