An exception is a runtime event that interrupts the normal flow of a program when an operation cannot complete as expected, such as dividing by zero, opening a missing file or receiving invalid input. In programming fundamentals, raising an exception transfers control to the nearest matching handler up the call stack instead of returning a value to the caller.
In most modern languages an exception is also a value, usually an object that carries a type, a message and a stack trace. The mechanism separates the code that detects a problem from the code that knows how to respond, which is the central idea of structured error handling.
How an exception travels through the call stack
When code throws or raises an exception, the current function stops at that point, and the runtime begins stack unwinding: it leaves the current function, then its caller, then the caller’s caller, until it finds a try block with a matching catch or except clause. If no handler exists anywhere on the stack, the exception becomes unhandled and the program or thread usually terminates with an error report.
Handlers are matched by type. Exception classes form a hierarchy, so a handler for a general type such as IOException also catches its subtypes such as FileNotFoundException. During unwinding, the runtime runs cleanup code: finally blocks in Java, C# and Python, destructors under RAII in C++, and with or using statements that release files, locks and connections.
The table below shows the main keywords that implement this mechanism in widely used languages.
| Language | Raise | Handle | Guaranteed cleanup |
|---|---|---|---|
| Java | throw | try / catch | finally, try-with-resources |
| C# | throw | try / catch | finally, using |
| Python | raise | try / except | finally, with |
| JavaScript | throw | try / catch, promise .catch() | finally |
| C++ | throw | try / catch | Destructors (RAII) |
Types of exceptions
Exceptions are classified by their cause and by whether the program can reasonably recover. Recoverable conditions come from the environment, such as a network timeout or a locked file, and a handler can retry or report them. Programming errors, such as a null reference or an index out of range, indicate a bug and should normally be fixed rather than caught.
Java makes this distinction part of the type system. Checked exceptions must be declared in the method signature or handled, while unchecked exceptions derive from RuntimeException and need no declaration. Most other languages, including C#, Python and Kotlin, treat all exceptions as unchecked.
| Category | Typical examples | Usual response |
|---|---|---|
| Input and validation | Malformed number, invalid date | Reject input and inform the user |
| Environment and I/O | Missing file, network timeout | Retry, fall back or report |
| Programming error | Null reference, index out of range | Fix the code, do not catch |
| Resource exhaustion | Out of memory, stack overflow | Usually terminate safely |
| Domain rule violation | Insufficient balance, duplicate order | Custom exception mapped to a business response |
Exceptions compared with other error-handling styles
Exceptions are one of several ways to signal failure, and languages choose differently. In C, functions usually return error codes and set errno, which callers must check after every call. Go returns an explicit error value alongside the result, while Rust uses a Result type that the compiler forces the caller to handle and reserves panics for unrecoverable bugs.
| Style | Example languages | Advantage | Drawback |
|---|---|---|---|
| Exceptions | Java, C#, Python, JavaScript | Clean main path, rich context | Hidden control flow |
| Error codes | C, many system APIs | Simple and fast | Easily ignored by callers |
| Error values | Go | Explicit at every call | Repetitive checking code |
| Result types | Rust, Swift, functional languages | Compiler-enforced handling | More verbose signatures |
The key trade-off is visibility against convenience. Exceptions keep the normal path readable and carry context automatically, but any call might throw, so the possible exits from a function are not visible in its code.
Performance and asynchronous code
In languages such as C++, Java and C#, the happy path is nearly free because modern runtimes use table-based or zero-cost exception models. The cost appears only when an exception is actually thrown: building the stack trace and unwinding frames can take microseconds to tens of microseconds, which is far slower than returning a value.
For that reason, exceptions should signal exceptional situations, not normal outcomes. Using exceptions for ordinary control flow, such as detecting the end of a loop, makes code slower and harder to follow. Python is a partial exception to this guideline, since its idiom of trying first and catching later is common and accepted.
Asynchronous code changes the propagation path. In JavaScript, an error inside a promise becomes a rejected promise rather than a synchronous throw, so it must be handled with await inside try/catch or with .catch(); otherwise it surfaces as an unhandled rejection that can crash a Node.js process.
Common mistakes when handling exceptions
Most problems with exceptions come from handling them too broadly or too early. Good handlers catch the narrowest relevant type, at the level of code that can actually decide what to do.
| Mistake | Consequence | Better practice |
|---|---|---|
| Empty catch block | Errors vanish silently | Log, rethrow or handle explicitly |
| Catching the base type everywhere | Bugs are masked as handled | Catch specific types |
| Losing the original cause | Root cause disappears from logs | Wrap with the cause attached |
| Exceptions for normal flow | Slow and unclear code | Use return values or checks |
| Leaking resources on error | Open files and locks pile up | Use finally, with or RAII |
Applications should still have a top-level handler that logs unhandled exceptions with their stack traces and returns a safe, generic message to users, so internal details do not leak through error pages.
Explained without jargon
A program follows its instructions step by step. Sometimes a step cannot be done, for example when a file it needs is missing. An exception is the program raising its hand and saying something went wrong here.
Instead of carrying on with bad data, the program stops that task and passes the problem upward. It keeps passing it until it reaches a part of the code that knows what to do. That part might show a clear message, try again later or save the work safely. The point is to deal with problems in one sensible place instead of everywhere at once.
A typical use case
An online store’s checkout service calls an external payment gateway. Occasionally the gateway does not answer within its five-second limit, and the HTTP client throws a timeout exception deep inside the payment module.
The developers do not catch the timeout where it occurs. Instead, the payment module wraps it in a custom PaymentUnavailableException with the order number attached, and the checkout controller catches that specific type. The controller retries once, then marks the order as pending and tells the customer that payment confirmation will follow by email. A top-level handler logs any other unexpected exception with its full stack trace. Customers no longer see generic error pages, and the team can trace each failure to its original cause.