Loading learning content...
Programming languages are not neutral tools—they embody philosophies about how programmers should work, what mistakes are common, and how those mistakes should be prevented. Nowhere is this more apparent than in error handling mechanisms.
From Java's pioneering checked exceptions to Rust's type-safe Result types, from Python's dynamic duck-typed exceptions to Go's explicit error returns, each language has made deliberate design choices that shape how developers think about and handle errors.
This page surveys the major approaches to error handling across popular programming languages, examining not just the syntax but the design philosophy behind each choice.
By the end of this page, you will understand how major programming languages handle the checked/unchecked question, the philosophical foundations of each approach, and how to adapt your error handling strategies when working in different languages.
Java remains the only mainstream language with a comprehensive checked exception system. Released in 1995, Java's exception model was revolutionary for its time and has profoundly influenced the debate about error handling ever since.
The Java Philosophy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
// Java's Exception Hierarchy// // Throwable (base class for all throwables)// ├── Error (unchecked - JVM/system problems, not meant to be caught)// │ ├── OutOfMemoryError// │ ├── StackOverflowError// │ └── ...// └── Exception (base for application exceptions)// ├── RuntimeException (unchecked - programming errors)// │ ├── NullPointerException// │ ├── IllegalArgumentException// │ ├── IndexOutOfBoundsException// │ └── ...// └── [All other Exception subclasses] (checked)// ├── IOException// ├── SQLException // ├── ParseException// └── ... // CHECKED EXCEPTION - Must be declared and handledpublic class FileService { // 'throws' clause is REQUIRED by compiler public String readFile(String path) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(path)); try { return reader.readLine(); } finally { reader.close(); } } // CALLER MUST handle or propagate public void processFile(String path) { try { String content = readFile(path); // Must handle IOException System.out.println(content); } catch (IOException e) { System.err.println("Failed to read file: " + e.getMessage()); } } // Alternative: propagate to caller public void processFileOrThrow(String path) throws IOException { String content = readFile(path); // Propagated System.out.println(content); }} // UNCHECKED EXCEPTION - No declaration requiredpublic class Calculator { // No 'throws' clause needed public int divide(int a, int b) { if (b == 0) { throw new IllegalArgumentException("Cannot divide by zero"); } return a / b; } // Caller may optionally handle public void calculate() { int result = divide(10, 0); // Compiles fine, throws at runtime }} // MODERN JAVA (try-with-resources, multi-catch)public class ModernJava { public void processMultipleResources(String path) { // try-with-resources handles cleanup automatically try (FileInputStream fis = new FileInputStream(path); BufferedReader reader = new BufferedReader( new InputStreamReader(fis))) { String line; while ((line = reader.readLine()) != null) { process(line); } } catch (FileNotFoundException e) { // Handle specific exception handleMissingFile(path); } catch (IOException e) { // Handle more general exception handleIOError(e); } } // Multi-catch for similar handling public void handleMultipleExceptions() { try { riskyOperation(); } catch (IOException | SQLException e) { // Handle both the same way logger.error("Operation failed", e); } }}| Aspect | Checked Exceptions | Unchecked Exceptions |
|---|---|---|
| Base Class | Exception (not RuntimeException) | RuntimeException, Error |
| Compiler Enforcement | Must declare or handle | No enforcement |
| Intended Use | Recoverable conditions | Programming errors, system failures |
| Method Signature | Appears in throws clause | Not in throws clause |
| Common Examples | IOException, SQLException | NullPointerException, IllegalArgumentException |
C#, designed by Anders Hejlsberg at Microsoft (released 2000), explicitly chose not to include checked exceptions despite having full knowledge of Java's approach. This was a conscious decision, not an oversight.
Anders Hejlsberg on the decision:
"The concern I have about checked exceptions is the handcuffs that it puts on programmers... In a sense, you're predicting what the caller is going to need, and very often you don't know."
The C# Philosophy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
// C# Exception Hierarchy// All exceptions are unchecked - no compiler enforcement // System.Exception (base class)// ├── System.SystemException (system-level exceptions)// │ ├── NullReferenceException// │ ├── ArgumentException// │ │ ├── ArgumentNullException// │ │ └── ArgumentOutOfRangeException// │ ├── InvalidOperationException// │ └── ...// ├── System.ApplicationException (deprecated legacy base)// └── Custom application exceptions // NO THROWS CLAUSE - all exceptions are uncheckedpublic class FileService{ // Documentation replaces compiler enforcement /// <summary> /// Reads the first line of a file. /// </summary> /// <param name="path">The file path to read.</param> /// <returns>The first line of the file.</returns> /// <exception cref="FileNotFoundException"> /// Thrown when the specified file does not exist. /// </exception> /// <exception cref="IOException"> /// Thrown when an I/O error occurs while reading. /// </exception> public string ReadFile(string path) { // No throws clause needed using var reader = new StreamReader(path); return reader.ReadLine(); } // Caller optionally handles public void ProcessFile(string path) { try { string content = ReadFile(path); Console.WriteLine(content); } catch (FileNotFoundException) { Console.Error.WriteLine($"File not found: {path}"); } catch (IOException e) { Console.Error.WriteLine($"IO error: {e.Message}"); } } // Or doesn't handle - compiles fine public void ProcessFileUnsafely(string path) { string content = ReadFile(path); // No compiler warning Console.WriteLine(content); }} // CUSTOM EXCEPTIONS in C#public class OrderNotFoundException : Exception{ public string OrderId { get; } public OrderNotFoundException(string orderId) : base($"Order not found: {orderId}") { OrderId = orderId; } public OrderNotFoundException(string orderId, Exception inner) : base($"Order not found: {orderId}", inner) { OrderId = orderId; }} // MODERN C# PATTERNSpublic class ModernCSharp{ // Pattern matching in catch (C# 6+) public void HandleWithPatternMatching() { try { RiskyOperation(); } catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound) { HandleNotFound(); } catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.Unauthorized) { HandleUnauthorized(); } catch (HttpRequestException e) { HandleOtherHttpError(e); } } // Exception filters (when clause) public void RetryableExceptionHandling() { try { CallExternalService(); } catch (ServiceException e) when (e.IsRetryable) { ScheduleRetry(); } catch (ServiceException e) { ReportPermanentFailure(e); } }}C# relies on XML documentation comments to communicate exception information. Tools like Visual Studio display this documentation, but the compiler doesn't enforce it. This places responsibility on developers to read documentation and write robust handling code.
Kotlin, developed by JetBrains and released in 2011, runs on the JVM and must interoperate with Java code that uses checked exceptions. Kotlin's solution is elegant: treat all exceptions as unchecked, but provide annotations for Java interop.
From the Kotlin documentation:
"Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result — decreased productivity and little or no increase in code quality."
The Kotlin Philosophy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
// KOTLIN - All exceptions are unchecked// No throws clause in method signatures class FileService { // No throws clause needed - compiles without any exception handling fun readFile(path: String): String { return File(path).readText() // Throws IOException - unchecked in Kotlin } // Caller may optionally handle fun processFile(path: String) { try { val content = readFile(path) println(content) } catch (e: FileNotFoundException) { println("File not found: $path") } catch (e: IOException) { println("IO error: ${e.message}") } } // Or not handle - compiles fine fun processFileUnsafe(path: String) { val content = readFile(path) // No compiler warning println(content) }} // JAVA INTEROP with @Throws// When Kotlin code is called from Java, use @Throws class DatabaseService { // Without @Throws: Java caller won't know about exception fun queryUnsafe(): List<User> { // throws SQLException internally return executeQuery("SELECT * FROM users") } // With @Throws: Java caller will see throws clause @Throws(SQLException::class) fun query(): List<User> { return executeQuery("SELECT * FROM users") } // Multiple exception types @Throws(IOException::class, ParseException::class) fun loadConfiguration(path: String): Config { val content = File(path).readText() return parseConfig(content) }} // KOTLIN IDIOMATIC PATTERNS// Prefer sealed classes and Result for expected failures sealed class FileResult { data class Success(val content: String) : FileResult() data class NotFound(val path: String) : FileResult() data class AccessDenied(val path: String) : FileResult() data class Error(val exception: Exception) : FileResult()} class IdiomsService { // Return sealed class instead of throwing fun readFileSafe(path: String): FileResult { return try { FileResult.Success(File(path).readText()) } catch (e: FileNotFoundException) { FileResult.NotFound(path) } catch (e: SecurityException) { FileResult.AccessDenied(path) } catch (e: Exception) { FileResult.Error(e) } } // Using Kotlin's Result type (since 1.3) fun readFileResult(path: String): Result<String> { return runCatching { File(path).readText() } } // Caller uses when/fold fun processWithResult(path: String) { val result = readFileResult(path) result.fold( onSuccess = { content -> println("Content: $content") }, onFailure = { exception -> println("Failed: ${exception.message}") } ) // Or with getOrNull/getOrElse val content = result.getOrElse { "default content" } // Or with getOrThrow (rethrow the exception) try { val strictContent = result.getOrThrow() } catch (e: Exception) { println("Rethrown: ${e.message}") } }} // EXTENSION FUNCTIONS for cleaner error handlinginline fun <T> tryOrNull(block: () -> T): T? { return try { block() } catch (e: Exception) { null }} // Usagefun example() { val content = tryOrNull { File("config.json").readText() } if (content != null) { processContent(content) } else { useDefaults() }}Python, being a dynamically typed language, takes a characteristically flexible approach to exception handling. All exceptions are unchecked, and the language embraces a philosophy often summarized as "Easier to Ask Forgiveness than Permission" (EAFP).
The Python Philosophy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
# Python Exception Hierarchy (simplified)## BaseException# ├── SystemExit# ├── KeyboardInterrupt# ├── GeneratorExit# └── Exception# ├── StopIteration# ├── ArithmeticError (ZeroDivisionError, OverflowError, ...)# ├── LookupError (IndexError, KeyError)# ├── AttributeError# ├── OSError (FileNotFoundError, PermissionError, ...)# ├── ValueError# ├── TypeError# └── ... many more # BASIC EXCEPTION HANDLINGdef read_file(path: str) -> str: """ Read and return file contents. Args: path: Path to the file to read. Returns: The file contents as a string. Raises: FileNotFoundError: If the file doesn't exist. PermissionError: If access is denied. UnicodeDecodeError: If the file isn't valid UTF-8. """ # No declaration in signature - documentation only with open(path, 'r', encoding='utf-8') as f: return f.read() def process_file(path: str) -> None: """Process a file with error handling.""" try: content = read_file(path) print(f"Content: {content[:100]}...") except FileNotFoundError: print(f"File not found: {path}") except PermissionError: print(f"Permission denied: {path}") except UnicodeDecodeError as e: print(f"Encoding error: {e}") except Exception as e: # Catch-all for unexpected exceptions print(f"Unexpected error: {type(e).__name__}: {e}") raise # Re-raise to let caller handle # EAFP vs LBYL (Easier to Ask Forgiveness vs Look Before You Leap) # LBYL style (more common in other languages)def get_value_lbyl(data: dict, key: str, default=None): if key in data: return data[key] return default # EAFP style (Pythonic)def get_value_eafp(data: dict, key: str, default=None): try: return data[key] except KeyError: return default # CONTEXT MANAGERS (like try-with-resources)from contextlib import contextmanager @contextmanagerdef managed_resource(name: str): """Context manager for automatic cleanup.""" resource = acquire_resource(name) try: yield resource except Exception as e: log_error(f"Error with resource {name}: {e}") raise finally: release_resource(resource) def use_managed_resource(): with managed_resource("database") as db: db.execute("SELECT * FROM users") # Resource automatically released, even on exception # CUSTOM EXCEPTIONSclass ApplicationError(Exception): """Base exception for application errors.""" pass class OrderError(ApplicationError): """Base exception for order-related errors.""" def __init__(self, order_id: str, message: str): self.order_id = order_id super().__init__(f"Order {order_id}: {message}") class OrderNotFoundException(OrderError): """Raised when an order cannot be found.""" def __init__(self, order_id: str): super().__init__(order_id, "Order not found") class OrderNotCancellableError(OrderError): """Raised when an order cannot be cancelled.""" def __init__(self, order_id: str, status: str): self.status = status super().__init__(order_id, f"Cannot cancel order in status {status}") # EXCEPTION CHAINING (Python 3)def process_order(order_id: str) -> None: try: order = database.find_order(order_id) except DatabaseError as e: # Chain the original exception raise OrderError(order_id, "Database lookup failed") from e # EXCEPTION GROUPS (Python 3.11+)def process_multiple_items(items: list) -> None: """Process items, collecting all errors.""" errors = [] results = [] for item in items: try: result = process_item(item) results.append(result) except Exception as e: errors.append(e) if errors: # Raise all errors together raise ExceptionGroup("Multiple processing errors", errors)Python's EAFP philosophy means it's often cleaner to try an operation and handle the exception than to check preconditions first. This is particularly true for dictionary access, file operations, and attribute access—places where the check itself could fail or race with the operation.
Go (Golang), designed at Google and released in 2009, takes a radically different approach: errors are values, not exceptions. Go does have a panic/recover mechanism, but it's reserved for truly exceptional cases. Normal error handling uses explicit return values.
The Go Philosophy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
package main import ( "errors" "fmt" "io/ioutil" "os") // GO ERROR HANDLING - Errors are values, not exceptions // Functions return error as last valuefunc readFile(path string) (string, error) { // os.ReadFile returns ([]byte, error) data, err := ioutil.ReadFile(path) if err != nil { return "", err // Return the error to caller } return string(data), nil // Return nil error on success} // Caller must explicitly check errorfunc processFile(path string) { content, err := readFile(path) if err != nil { // Handle error - cannot ignore without explicit decision fmt.Printf("Error reading file: %v", err) return } fmt.Printf("Content: %s", content)} // IGNORING ERRORS - Explicit and obviousfunc dangerouslyIgnoreError(path string) { content, _ := readFile(path) // Underscore explicitly ignores error fmt.Println(content) // May be empty if error occurred} // CUSTOM ERROR TYPES// Simple error with errors.Newvar ErrNotFound = errors.New("not found")var ErrPermissionDenied = errors.New("permission denied") // Error with context using fmt.Errorffunc findUser(id string) (*User, error) { user := database.FindByID(id) if user == nil { return nil, fmt.Errorf("user not found: %s", id) } return user, nil} // Custom error type for rich error handlingtype OrderError struct { OrderID string Op string Err error} func (e *OrderError) Error() string { return fmt.Sprintf("order %s: %s: %v", e.OrderID, e.Op, e.Err)} func (e *OrderError) Unwrap() error { return e.Err} func cancelOrder(orderID string) error { order, err := findOrder(orderID) if err != nil { return &OrderError{ OrderID: orderID, Op: "cancel", Err: err, } } if order.Status != "pending" { return &OrderError{ OrderID: orderID, Op: "cancel", Err: fmt.Errorf("invalid status: %s", order.Status), } } return nil} // ERROR WRAPPING (Go 1.13+)func processOrder(orderID string) error { order, err := findOrder(orderID) if err != nil { // Wrap error with context using %w return fmt.Errorf("processing order %s: %w", orderID, err) } return nil} // ERROR CHECKING with errors.Is and errors.As (Go 1.13+)func handleOrderError(err error) { // Check for specific sentinel error if errors.Is(err, ErrNotFound) { fmt.Println("Order not found - create new one") return } // Extract specific error type var orderErr *OrderError if errors.As(err, &orderErr) { fmt.Printf("Order %s failed during %s", orderErr.OrderID, orderErr.Op) return } // Generic error fmt.Printf("Unknown error: %v", err)} // PANIC/RECOVER - Only for truly exceptional casesfunc mustReadConfig(path string) Config { config, err := readConfig(path) if err != nil { // Panic for unrecoverable startup failures panic(fmt.Sprintf("cannot read config: %v", err)) } return config} // Recover from panic (typically in middleware/top-level)func safeHandler(handler func()) { defer func() { if r := recover(); r != nil { fmt.Printf("Recovered from panic: %v", r) } }() handler()}Go's error handling is intentionally verbose. The language designers believe that the verbosity makes error handling visible and explicit, preventing the 'hidden' error paths that exceptions can create. Every error is handled at the point of the call, making control flow obvious.
Rust, released in 2015, represents perhaps the most sophisticated approach to error handling among mainstream languages. Rust uses algebraic data types (Result and Option) rather than exceptions, combined with powerful pattern matching and the ? operator for propagation.
The Rust Philosophy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
// RUST ERROR HANDLING - Type-safe with Result and Option use std::fs::File;use std::io::{self, Read, BufReader, BufRead};use std::num::ParseIntError; // Result<T, E> - Either success value T or error value E// enum Result<T, E> {// Ok(T),// Err(E),// } // Functions return Result instead of throwingfn read_file(path: &str) -> Result<String, io::Error> { let mut file = File::open(path)?; // ? propagates error let mut content = String::new(); file.read_to_string(&mut content)?; Ok(content) // Wrap success value in Ok} // PATTERN MATCHING for error handlingfn process_file(path: &str) { match read_file(path) { Ok(content) => { println!("Content: {}", content); } Err(e) => { // Pattern match on error kind match e.kind() { io::ErrorKind::NotFound => { println!("File not found: {}", path); } io::ErrorKind::PermissionDenied => { println!("Permission denied: {}", path); } _ => { println!("IO error: {}", e); } } } }} // THE ? OPERATOR - Ergonomic error propagationfn read_first_line(path: &str) -> Result<String, io::Error> { let file = File::open(path)?; // Returns Err if open fails let reader = BufReader::new(file); let mut line = String::new(); reader.lines() .next() .ok_or_else(|| io::Error::new( io::ErrorKind::UnexpectedEof, "Empty file" ))??; // ? on Option, then on Result Ok(line)} // Equivalent without ? operator (verbose)fn read_first_line_verbose(path: &str) -> Result<String, io::Error> { let file = match File::open(path) { Ok(f) => f, Err(e) => return Err(e), }; // ... much more verbose todo!()} // CUSTOM ERROR TYPESuse std::fmt;use std::error::Error; #[derive(Debug)]enum OrderError { NotFound(String), InvalidStatus { order_id: String, status: String }, DatabaseError(String),} impl fmt::Display for OrderError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { OrderError::NotFound(id) => write!(f, "Order not found: {}", id), OrderError::InvalidStatus { order_id, status } => write!(f, "Order {} has invalid status: {}", order_id, status), OrderError::DatabaseError(msg) => write!(f, "Database error: {}", msg), } }} impl Error for OrderError {} // Using custom errorfn cancel_order(order_id: &str) -> Result<(), OrderError> { let order = find_order(order_id) .ok_or_else(|| OrderError::NotFound(order_id.to_string()))?; if order.status != "pending" { return Err(OrderError::InvalidStatus { order_id: order_id.to_string(), status: order.status.to_string(), }); } Ok(())} // OPTION<T> for absence (like Optional/Maybe)// enum Option<T> {// Some(T),// None,// } fn find_user(id: &str) -> Option<User> { // Returns Some(user) or None users.get(id).cloned()} fn process_user(id: &str) { match find_user(id) { Some(user) => println!("Found: {}", user.name), None => println!("User not found"), } // Or with combinators let name = find_user(id) .map(|u| u.name) .unwrap_or_else(|| "Unknown".to_string());} // THISERROR AND ANYHOW - Popular error handling crates// thiserror - for library error typesuse thiserror::Error; #[derive(Error, Debug)]enum PaymentError { #[error("insufficient funds: needed {needed}, available {available}")] InsufficientFunds { needed: u64, available: u64 }, #[error("invalid card: {0}")] InvalidCard(String), #[error(transparent)] DatabaseError(#[from] DatabaseError),} // anyhow - for application error handlinguse anyhow::{Context, Result as AnyhowResult}; fn load_config(path: &str) -> AnyhowResult<Config> { let content = std::fs::read_to_string(path) .context("Failed to read config file")?; let config: Config = serde_json::from_str(&content) .context("Failed to parse config JSON")?; Ok(config)}Rust's approach combines the explicitness of Go with compile-time enforcement stronger than Java's checked exceptions. You cannot ignore a Result—the compiler forces you to handle it, unwrap it (which panics on error), or propagate it with ?. This makes 'forgotten error handling' impossible.
Swift, Apple's language released in 2014, takes a middle ground between Java's checked exceptions and the fully unchecked approach. Swift has exceptions (called errors) that must be handled, but doesn't require declaring the specific types that can be thrown.
The Swift Philosophy:
throwstry, try?, or try!123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
// SWIFT ERROR HANDLING - throws keyword without specific types import Foundation // Errors conform to Error protocolenum FileError: Error { case notFound(path: String) case permissionDenied(path: String) case invalidEncoding} enum OrderError: Error { case notFound(orderId: String) case invalidStatus(orderId: String, status: String) case paymentFailed(reason: String)} // THROWING FUNCTIONS - marked with 'throws'// Note: doesn't specify WHICH errors can be thrownfunc readFile(path: String) throws -> String { guard FileManager.default.fileExists(atPath: path) else { throw FileError.notFound(path: path) } guard FileManager.default.isReadableFile(atPath: path) else { throw FileError.permissionDenied(path: path) } // This also throws, but any Error type return try String(contentsOfFile: path, encoding: .utf8)} // CALLING THROWING FUNCTIONS // Option 1: do-catch (like try-catch)func processFile(path: String) { do { let content = try readFile(path: path) print("Content: (content)") } catch FileError.notFound(let path) { print("File not found: (path)") } catch FileError.permissionDenied(let path) { print("Permission denied: (path)") } catch { // 'error' is implicitly available print("Unexpected error: (error)") }} // Option 2: try? - converts to optional (returns nil on error)func processFileOptional(path: String) { if let content = try? readFile(path: path) { print("Content: (content)") } else { print("Failed to read file") }} // Option 3: try! - force unwrap (crashes on error)func processFileForce(path: String) { // Only use when you're CERTAIN it won't fail let content = try! readFile(path: path) print(content)} // PROPAGATING ERRORS// Just add 'throws' to your functionfunc processMultipleFiles(paths: [String]) throws -> [String] { var contents: [String] = [] for path in paths { let content = try readFile(path: path) // Propagates contents.append(content) } return contents} // RETHROWS - for higher-order functionsfunc withRetry<T>(attempts: Int, operation: () throws -> T) rethrows -> T { var lastError: Error? for _ in 0..<attempts { do { return try operation() } catch { lastError = error } } throw lastError!} // RESULT TYPE (Swift 5+) - Alternative to throwingenum Result<Success, Failure: Error> { case success(Success) case failure(Failure)} func readFileResult(path: String) -> Result<String, FileError> { do { let content = try readFile(path: path) return .success(content) } catch let error as FileError { return .failure(error) } catch { return .failure(.invalidEncoding) }} func processWithResult(path: String) { let result = readFileResult(path: path) switch result { case .success(let content): print("Content: (content)") case .failure(let error): print("Error: (error)") } // Or with Result methods let content = result.map { $0.uppercased() } let safeContent = try? result.get()}Now that we've examined each language's approach, let's consolidate the comparison to help you adapt your error handling strategies across languages.
| Feature | Java | C# | Kotlin | Python | Go | Rust | Swift |
|---|---|---|---|---|---|---|---|
| Checked Exceptions | Yes | No | No | No | N/A | N/A | Partial |
| Unchecked Exceptions | Yes | Yes | Yes | Yes | panic | panic | Yes |
| Returns Error Values | No | No | Result | No | Yes | Result | Result |
| Compiler Enforces Handling | Partial | No | No | No | No | Yes | Yes |
| Signature Includes Errors | throws X | No | @Throws | No | error return | Result<T,E> | throws |
| Try-Catch Syntax | Yes | Yes | Yes | Yes | No | No | do-catch |
You now understand how major programming languages approach the checked/unchecked exception dichotomy. This knowledge enables you to write idiomatic error handling code in each language and to make informed decisions when designing error handling strategies for new systems.