In short
Exception handling is the language mechanism that detects an abnormal condition at run time, transfers control to a handler, and releases resources on the way, so a program fails predictably instead of corrupting data or crashing. Debugging is the systematic process of locating and removing the defect that produced a wrong result or an unhandled exception. Both are examined in university programming and software-engineering papers, appear as output-prediction and error-classification questions in GATE CS and in company aptitude rounds, and are tested in every technical interview at TCS, Infosys, Wipro, Cognizant, Accenture, Zoho, Flipkart, Zomato, Freshworks and the GCCs in Bengaluru, Pune and Hyderabad. On the job they decide whether a production incident takes five minutes or five hours, which is why log hygiene, stack traces and reproducible test cases are reviewed in code review.
Explained
Errors, defects and failures - the vocabulary
- Error (mistake): a human action that produces a wrong artefact; defect (bug/fault): the wrong code or data in the artefact; failure: the observable deviation when that defect executes. IEEE 1044 and ISTQB use exactly this chain - an interviewer may ask it verbatim.
- Classification by detection time:
- Compile-time / syntax errors - missing semicolon, type mismatch; caught by the compiler, program never runs.
- Linker errors - "undefined reference to
foo", declared but never defined, or a library not linked. - Run-time errors - divide by zero, null dereference, array out of bounds, file not found; these are what exceptions model.
- Logic / semantic errors - program runs and prints a wrong answer (wrong formula, off-by-one). The compiler cannot help; only testing and debugging can.
- Exception vs error condition: an exception is an *expected-but-abnormal* event (file missing, bad input, network timeout) that the program can reasonably handle; a programming bug (index out of range, null pointer) should be *fixed*, not swallowed.
- Undefined behaviour (C/C++): out-of-bounds write, use of a dangling pointer, signed overflow - there is no exception; the program may appear to work and fail later. This is why C debugging needs sanitizers and Valgrind, not try/catch.
Mechanism: how an exception travels
- A throw point creates an exception object (
throw,raise, or thrown by the runtime, e.g. division by zero). - The runtime searches the call stack outward for the nearest enclosing
trywith a matching handler (match by type, derived types matching a base handler). - Stack unwinding: each frame between the throw point and the handler is destroyed. In C++ automatic objects' destructors run (this is what makes RAII safe); in Java/Python/JS local references simply go out of scope and the GC reclaims them later, and
finallyblocks run. - The handler executes; control then continues after the whole try statement - not back at the throw point (no resumption model in C++/Java/Python).
- If no handler is found anywhere: Java prints the stack trace and kills the thread; Python prints a traceback and exits with status 1; C++ calls
std::terminate→abort. - Cost model: modern C++/Java implementations are "zero-cost" on the non-throwing path (tables, not runtime checks), but a *thrown* exception is expensive. So never use exceptions for ordinary loop control in C++/Java. Python is different -
forloops end viaStopIterationinternally, and the "easier to ask forgiveness than permission" (EAFP) style is idiomatic.
Syntax and features by language
- C - no exceptions at all. Error signalling is by return code and
errno: check the return value first, then readerrnoand print withperror()/strerror(errno). Never testerrnobefore checking the return value.setjmp/longjmpgives a crude non-local jump (no destructors, easy to corrupt);signal()/sigaction()trapsSIGSEGV,SIGFPE,SIGINTbut a signal handler must only do async-signal-safe work. - C++ -
try { } catch (const T& e) { } catch (...) { },throw expr;. Hierarchy:std::exception→logic_error(invalid_argument,out_of_range,domain_error),runtime_error(overflow_error,system_error), plusbad_alloc,bad_cast. Catch by const reference to avoid copying and slicing.throw;inside a handler rethrows the same object. **noexcept** declares a function non-throwing (if it throws,std::terminate). Destructors must not throw. Dynamic exception specifications (throw(int)) were deprecated in C++11 and removed in C++17. Exception-safety guarantees: *no-throw*, *strong* (commit-or-rollback), *basic* (no leak, valid state), *none*. - RAII - the C++ answer to
finally: wrap every resource (memory, file, lock, socket) in an object whose destructor releases it -std::unique_ptr,std::lock_guard,std::fstream. Then unwinding cleans up automatically and nofinallyis needed. - Java -
try/catch/finally,throw(statement, throws one object) vs **throws** (clause declaring what a method may propagate). Hierarchy:Throwable→Error(OutOfMemoryError,StackOverflowError- do not catch) andException. Checked =Exceptionminus theRuntimeExceptionsubtree; must be caught or declared (IOException,SQLException,ClassNotFoundException). Unchecked =RuntimeExceptionand below (NullPointerException,ArithmeticException,ArrayIndexOutOfBoundsException,NumberFormatException,IllegalArgumentException,ClassCastException,ConcurrentModificationException).- Catch blocks are tried in order, so a subclass must be caught before its superclass - the reverse is a compile error. Multi-catch
catch (IOException | SQLException e)(Java 7) makeseimplicitly final. - try-with-resources
try (BufferedReader br = new BufferedReader(...)) { }auto-closes anyAutoCloseableand records close-time failures as suppressed exceptions (getSuppressed()); it replaces the error-pronefinally { if (x != null) x.close(); }. - Custom exception: extend
Exception(checked, caller must act) orRuntimeException(programming/validation error); always provide a(String message, Throwable cause)constructor so the cause is chained (getCause()).finally≠finalize()(an object-destruction hook, deprecated since Java 9).
- Catch blocks are tried in order, so a subclass must be caught before its superclass - the reverse is a compile error. Multi-catch
- Python -
try / except / else / finally;elseruns only if no exception was raised,finallyalways.except ValueError as e:, tuple formexcept (KeyError, IndexError):, bareexcept:also catchesKeyboardInterrupt/SystemExitand is bad practice - useexcept Exception.raise, bareraiseto re-raise, **raise X from err** sets__cause__for explicit chaining (implicit chaining uses__context__).BaseException→Exception,KeyboardInterrupt,SystemExit,GeneratorExit; underException:ArithmeticError(ZeroDivisionError),LookupError(IndexError,KeyError),OSError(FileNotFoundError,PermissionError),ValueError,TypeError,AttributeError,ImportError,StopIteration,RecursionError.withstatements (context managers) are Python's RAII.assertfor invariants - disabled bypython -O. Python 3.11 addedExceptionGroupwithexcept*ande.add_note(). - JavaScript -
try/catch(e)/finally, optional bindingcatch { };throwaccepts any value but always throw anError. Built-ins:Error,TypeError,ReferenceError,RangeError,SyntaxError,URIError. Asynchronous errors do not reach a surroundingtryunless youawait: use.catch()on promises ortry { await p } catch.Error.cause(ES2022) chains; unhandled rejections surface viaprocess.on('unhandledRejection')orwindow.onunhandledrejection.
Designing error handling well
- Throw early, catch late: detect at the lowest layer, handle where there is enough context to decide (retry, default, user message, abort).
- Never swallow: an empty
catch { }orexcept: passdestroys evidence and is the single most common review rejection. At minimum log with the stack trace. - Do not use exceptions for flow control in C++/Java; validate input instead. Conversely do not pre-check everything in Python when EAFP is cheaper.
- Fail fast on programmer errors (assertions, invalid state) and degrade gracefully on environmental errors (network, disk).
- Translate across layers: catch
SQLExceptionin the DAO and rethrow aDataAccessExceptionwith the cause attached, so upper layers do not depend on the database. - Idempotency and cleanup: anything opened in
trymust be released infinally/with/destructor; the order of release is the reverse of acquisition. - Retry with backoff only for transient faults (timeouts, 5xx, deadlock), with a bounded attempt count - a blind retry loop turns one failure into an outage.
The debugging process and tools
- Reproduce reliably - exact input, build, environment; shrink to a minimal failing case.
- Observe - read the actual message, exception type, line number and full stack trace, bottom-up to find the first frame in *your* code.
- Localise by divide-and-conquer - bisect the input, the code path, or the commit history (
git bisect, ~log₂N builds). - Form a hypothesis, make *one* change, and verify; revert if it does not help.
- Fix the cause, not the symptom; add a regression test that fails before and passes after; then look for the same defect pattern elsewhere.
- Techniques: print/trace debugging, interactive breakpoints, conditional breakpoints and watchpoints, backtracking, cause-elimination, rubber-duck explanation, post-mortem debugging from a core dump or crash log.
- gdb (compile with
-g -O0):break file:line,run,next,step,finish,continue,print x,backtrace/bt,info locals,watch var,gdb ./a.out core. - Python:
python -m pdb prog.py, orbreakpoint()in code;b,n,s,c,l,p/pp,w,q;traceback.print_exc(),faulthandler. - Java: IDE debugger (IntelliJ/Eclipse),
jdb,-XX:+HeapDumpOnOutOfMemoryError, thread dumps viajstack,jmap/VisualVMfor leaks. - JS: Chrome DevTools sources panel,
debugger;statement,node --inspect, source maps for transpiled code. - Memory and UB tools (C/C++):
valgrind --leak-check=full(leaks, invalid reads/writes), AddressSanitizer/UBSan (-fsanitize=address,undefined),gcc -Wall -Wextra -Werror. - Static analysis and quality gates:
cppcheck,clang-tidy,pylint/flake8/mypy, ESLint, SpotBugs, SonarQube in the CI pipeline - they find defects without executing code. - Logging over printing in production: levels TRACE/DEBUG/INFO/WARN/ERROR/FATAL; Python
logging, Java SLF4J + Log4j2/Logback, structured JSON logs shipped to ELK/Grafana Loki. Include a correlation/request id; never log passwords, OTPs, card data, Aadhaar or other personal data. The CERT-In directions of April 2022 require Indian service providers to maintain ICT system logs (commonly cited as 180 days, within India) and report specified incidents within 6 hours; the DPDP Act 2023 adds data-minimisation duties - treat log contents as personal data. Check the current text before quoting these in practice, as compliance detail is updated.
Key points
- Exception - a run-time abnormal condition represented as an object and handled by a matching handler up the call stack.
- throw vs throws (Java) -
throwraises one object;throwsdeclares what a method may propagate. - Checked exception - compile-time enforced (
IOException,SQLException); must be caught or declared. - Unchecked exception -
RuntimeExceptionfamily; usually signals a bug (NullPointerException,ArrayIndexOutOfBoundsException). - Error (Java) -
OutOfMemoryError,StackOverflowError; recovery is not expected, do not catch. - finally - always executes (cleanup); the only escapes are
System.exit, JVM crash or an infinite loop intry. - **A
returninfinallyoverrides** the value or exception fromtry/catch- a classic trap. - Catch order - subclass handler before superclass, else unreachable code / compile error in Java and C++ warning.
- Stack unwinding - frames between throw and handler are destroyed; C++ destructors run, which is why RAII replaces
finally. - Exception chaining - keep the original cause (
getCause(),__cause__,Error.cause); never lose the root stack trace. - try-with-resources - auto-closes
AutoCloseable, reports close failures as suppressed exceptions. - Python else/finally -
elseonly on success,finallyalways;withis the resource idiom. - C has no exceptions - return codes +
errno+perror;setjmp/longjmpis a last resort. - Segmentation fault - OS signal from an invalid memory access (null/dangling pointer, out-of-bounds, stack overflow), not a catchable exception.
- Assertions - check invariants during development; compiled out with
NDEBUG/python -O, so never use them for input validation. - Debug cycle - reproduce → observe → localise (bisect) → hypothesise → fix cause → regression test.
- Heisenbug - a defect that disappears when observed (timing, uninitialised memory, optimisation level); suspect concurrency or UB.
- Zero-cost model - no overhead until an exception is thrown, so throwing in a hot loop is the performance mistake, not the
tryblock.
Formulas and examples
Defect Removal Efficiency DRE = Dbefore / (Dbefore + Dafter) × 100 %, Dbefore = defects found before release, Dafter = defects found by users after release.
Bisection steps = ⌈log₂ N⌉, N = number of suspect commits, inputs or code regions. Unit: builds/tests.
Branch coverage = (branches executed / total branches) × 100 %; Statement coverage uses statements.
MTTR = total repair time / number of incidents; Availability = MTBF / (MTBF + MTTR).
EAFP cost = N × p × Chandle; LBYL cost = N × Ccheck; N = records, p = failure fraction, C = cost per operation.
Example 1 - Java try/catch/finally trace.
int x = 0;
try {
x = 1;
int[] a = new int[2];
a[5] = 9; // throws ArrayIndexOutOfBoundsException
x = 2; // never executed
} catch (ArrayIndexOutOfBoundsException e) {
x = 3;
} catch (Exception e) {
x = 4; // not reached: first handler matched
} finally {
x = x + 10;
}
System.out.println(x);
Step 1: x = 1. Step 2: the bad store throws, so x = 2 is skipped. Step 3: the first matching handler sets x = 3. Step 4: finally adds 10. Output: 13.
**Example 2 - finally overriding a return.**
static int f() { try { return 1; } finally { System.out.println("cleanup"); } } // prints cleanup, returns 1
static int g() { try { return 1; } finally { return 2; } } // returns 2
static int h() { try { throw new RuntimeException(); } finally { return 3; } } // returns 3, exception discarded
In f the return value 1 is computed, then finally runs, then the method returns 1. In g the finally completes with its own return, so 2 is returned. In h the finally's return swallows the exception and the method returns 3 - which is exactly why a return inside finally is banned by most style guides.
Example 3 - C++ stack unwinding order.
struct Res { std::string n; ~Res() { std::cout << "~" << n << " "; } };
void inner() { Res b{"B"}; throw std::runtime_error("boom"); }
void outer() { Res a{"A"}; inner(); }
int main() {
try { outer(); }
catch (const std::exception& e) { std::cout << "caught " << e.what(); }
}
B is destroyed first (innermost frame), then A, then the handler runs. **Output: ~B ~A caught boom.** With raw new/delete instead of RAII both allocations would leak.
Example 4 - git bisect arithmetic. A regression appears somewhere in the last 2,000 commits on a 35,000-LOC service.
- Linear search: up to 2,000 builds. Bisection: ⌈log₂ 2000⌉ = 11 builds (2¹⁰ = 1,024 < 2,000 ≤ 2,048 = 2¹¹).
- At 4 minutes per build-and-test, that is 11 × 4 = 44 minutes instead of up to 2,000 × 4 ≈ 133 hours.
- If the release had 42 known defects in 35,000 LOC, defect density = 42 / 35 = 1.2 defects per KLOC.
- If 180 defects were found in testing and 20 by users, DRE = 180 / 200 × 100 = 90%.
Example 5 - EAFP vs LBYL in Python. A batch of N = 1,00,000 records is converted with int(s); p = 2% are malformed. Suppose raising and handling one exception costs 50 µs and a pre-validating regex check costs 2 µs per record.
- LBYL (check every record): 1,00,000 × 2 µs = 2,00,000 µs = 0.20 s.
- EAFP (
try/except ValueError, cost only on failure): 0.02 × 1,00,000 = 2,000 failures × 50 µs = 1,00,000 µs = 0.10 s. - EAFP wins by 2×. Break-even failure fraction: p × 50 = 2 → p = 4%; above that, pre-checking is cheaper. (The µs figures are illustrative inputs for the calculation, not measured constants.)
| Feature | C | C++ | Java | Python | JavaScript |
|---|---|---|---|---|---|
| Mechanism | return code + errno | try/catch/throw | try/catch/finally/throw | try/except/else/finally/raise | try/catch/finally/throw |
| Compile-time enforcement | none | none | checked exceptions | none | none |
| Cleanup idiom | manual goto cleanup | RAII (destructors) | finally, try-with-resources | with (context manager) | finally |
| Catch-all | - | catch (...) | catch (Throwable t) | except BaseException | catch (e) |
| Chaining | - | std::nested_exception | initCause / cause arg | raise X from e | Error.cause |
| Typical debugger | gdb, Valgrind, ASan | gdb, ASan | IDE, jdb, jstack | pdb | DevTools, node --inspect |
Where it is used
- IT services and banking back ends (TCS, Infosys, Wipro, HCLTech, LTIMindtree): Java/Spring services where
SQLExceptionand timeout handling, retry policy and structured logs are mandatory review items. - Fintech and UPI-scale payments (Razorpay, PhonePe, Paytm, NPCI integrations): idempotent retries, exception translation into gateway error codes, reconciliation of failed-but-charged transactions.
- Product and SaaS engineering (Zoho, Freshworks, Flipkart, Swiggy): Sentry/New Relic error dashboards, on-call triage from stack traces, SLO/MTTR tracking.
- Embedded, automotive and telecom firmware (Bosch, KPIT, Tata Elxsi, Qualcomm India): C with no exceptions - error codes, watchdogs, MISRA-C rules, JTAG/gdb hardware debugging, crash-log decoding.
- QA, SDET and DevOps/SRE roles: writing negative test cases that assert the right exception (
pytest.raises, JUnitassertThrows), CI gates on static analysis and coverage. - Government and regulated systems (UIDAI, GSTN, IRCTC-scale portals): audit logging, 6-hour CERT-In incident reporting, log retention and masking of personal data under the DPDP Act 2023.
Interview questions
- Q: Difference between checked and unchecked exceptions in Java? A: Checked exceptions are verified by the compiler and must be caught or declared with
throws(IOException); unchecked ones extendRuntimeException, need no declaration and usually indicate a programming bug. - Q: Will
finallyalways execute? A: Yes, except when the JVM/process exits (System.exit,os._exit), the thread is killed, the machine crashes, or thetryblock never terminates. - Q: What happens if both
tryandfinallyreturn a value? A: Thefinallyblock's return wins and silently discards thetryvalue or a pending exception - which is why returning fromfinallyis prohibited in practice. - Q: Why must a C++ destructor not throw? A: During stack unwinding a second exception while one is active calls
std::terminate; destructors should benoexceptand catch internally. - Q: How does C handle errors without exceptions? A: Functions return a sentinel value and set
errno; callers check the return value first, then report withperror/strerror.setjmp/longjmpcan jump out but skips all cleanup. - Q: Can you catch a segmentation fault? A: Not as a language exception - it arrives as
SIGSEGV; you can install a handler to log and abort, but the process state is already undefined, so recovery is unsafe. - Q: Difference between
throwandthrows? A:throwis a statement that actually raises an exception object;throwsis a method-signature clause declaring the checked exceptions the method may propagate. - Q: What is exception chaining and why does it matter? A: Wrapping a low-level exception as the *cause* of a higher-level one (
raise X from e,new ServiceException(msg, e)) so the original stack trace and root cause survive the translation. - Q:
finallyvsfinalize()in Java? A:finallyis a block that runs aftertry/catchfor cleanup;finalize()was a GC callback on an object before collection, unreliable and deprecated since Java 9. - Q: Why is
except: passbad? A: It hides every failure includingKeyboardInterruptandSystemExit, leaves the program in an unknown state and removes all diagnostic evidence; catch a specific type and log it. - Q: Should exceptions be used for normal control flow? A: Not in C++/Java, where a throw is far costlier than a branch; in Python the EAFP style is idiomatic and often faster when failures are rare.
- Q: How would you debug a bug that appears only in production? A: Reproduce from logs and the request id, compare build and configuration, add targeted structured logging or feature-flagged tracing, inspect a heap/core dump or APM trace, then bisect recent commits and lock the fix with a regression test.
Common mistakes
- Writing an empty or over-broad handler (
catch (Exception e) { },except: pass) so the real failure disappears. - Catching a superclass before its subclass, making the specific handler unreachable - a compile error in Java.
- Returning from, or throwing inside,
finallyand thereby discarding the original exception. - Printing with
e.printStackTrace()orprint(e)only, losing the type, the cause or the trace that the logger would have kept. - Using
assertto validate user input - assertions vanish under-O/NDEBUG, so the check silently disappears in release builds. - Expecting
try/catcharound an un-awaited promise or a thread's body to catch the error: asynchronous and other-thread exceptions do not propagate to the caller'stry. - Debugging an optimised binary without
-g, or changing several things at once so the result of the experiment cannot be attributed. - Treating a compiler warning as noise - most C/C++ run-time crashes were already flagged by
-Wall -Wextra.
Related topics
C programming fundamentals · Pointers and dynamic memory · Java essentials · Python essentials · Object-oriented programming concepts · Compilation, linking and memory layout