Error Handling — Cross-Language Comparison

This note compares the error-handling models a programmer actually picks between when designing an API — exceptions vs error-as-value vs Optional/Maybe vs error effects vs panic-and-restart — across all 53 deep language notes in the library. Each section gives the canonical mechanism, its real cost (allocation, control-flow disruption, type-system burden), where it dominates, and where it breaks down. The closing decision tree maps application type and safety requirement onto the right model.

See also

1. The five families

Five families dominate every language’s error-handling story. Most modern languages mix two or more; the boundary between “exception” and “effect” has blurred since 2018.

FamilyMechanismCostLanguages
Exceptions (unchecked)throw / catch / finally; stack unwindhigh (allocate exception + capture stack + unwind frames)Python, Java (RuntimeException), JavaScript, C#, Ruby, Kotlin, Scala, Groovy, PHP
Exceptions (checked)declared in signature; compiler enforceshigh + signature churnJava (subclass of Exception not RuntimeException) — only mainstream language with checked
Error-as-valueResult<T,E> / Either<E,A> / Go (T, error); explicit unwraplow (no unwind), high syntactic noiseRust, Go, Haskell, OCaml, F#, Scala (Try/Either), Swift (Result)
Optional / MaybeOption<T> / Maybe a / T?; null replacementlow (single bit)Haskell, OCaml, Scala, Swift, Kotlin (T?), Rust (Option<T>), F#
Effect systemsdeclared in type; algebraic effects with handlersmedium (one-shot or multi-shot continuations)Koka, Eff, Effekt; Effect-TS (TS library); Java JEP 453 structured concurrency partially
Panic / abortcrash the unit (thread, process, actor) and let supervisor restartlow (no recovery)Rust panic, Go panic, Erlang exit, Elixir raise, Pony — Erlang’s “let it crash” philosophy
Conditions / restartsnon-local resumable handlermediumCommon Lisp (canonical), Dylan
Returning sentinel-1, NULL, errnovery low + huge footgunC, COBOL
Process / shell exit code$?very lowBash, PowerShell pipelines

2. Exceptions — unchecked

Throw an object, unwind the stack until a matching handler catches. Allocation + stack capture is expensive (microseconds in Python, hundreds of ns in Java HotSpot post-2018). The control-flow surprise — any line might exit your function — is the design tension.

LanguageHierarchy rootTry/catch syntaxNotesLinked note
PythonBaseException (then Exception)try/except/else/finallyexceptions for control flow encouraged (StopIteration); __cause__ chaining via raise X from Y; ExceptionGroup + except* for parallel (PEP 654, 3.11+)python
JavaScriptError (subclass for TypeError, RangeError)try/catch/finally; catch (err) { ... } (no type filter without instanceof)AggregateError for Promise.any; unhandled-rejection eventjavascript
TypeScriptinherits JSsameTS does not type the catch clause — caught value is unknown since TS 4.4 defaulttypescript
JavaThrowableError (fatal) / Exception (checked) / RuntimeException (unchecked)try/catch/finally; try-with-resources (AutoCloseable)only mainstream lang with checked exceptions — must declare in throws; controversial since 2000sjava
[[Languages/csharp|C#]]Exceptiontry/catch/finally; when filter clause (catch (E e) when (cond) { ... }) since C# 6exception filters preserve stack — preferred to if (cond) throw; inside catchcsharp
RubyException (then StandardError)begin/rescue/else/ensurebare rescue catches StandardError; raise re-raisesruby
Kotlininherits Java Throwabletry/catch/finally; try is an expression (returns value)no checked exceptions even when calling Java; coroutine errors propagate via CoroutineExceptionHandlerkotlin
Scalainherits Java Throwabletry/catch/finally; catch uses pattern matchTry[A] / Either[E,A] / Validated[E,A] (Cats) are functional-style alternativesscala
Groovyinherits Javasameno checked exceptions in Groovy codegroovy
PHPThrowable (PHP 7+)try/catch/finally; multi-catch catch (A | B $e) since 8.0finally runs even after return; Error types separate from Exceptionphp
Perldie/$@; Try::Tiny/Try::Catch moduleseval { ... }; if ($@) { ... } (legacy) or try { ... } catch ($e) { ... } (5.34+)core try/catch since 5.34 (2021)perl
DartObject (anything can be thrown); idiomatic Exception/Errortry/on/catch/finally (on TypeName catch (e))Errors = programmer mistakes (uncaught preferred); Exceptions = runtime conditionsdart
Luaerror objects (string or table); pcall/xpcalllocal ok, err = pcall(fn, args); no try/catchxpcall lets you pass a handler before unwindlua
Rconditions (extends Lisp model); tryCatch + withCallingHandlerstryCatch({...}, error = function(e) {...})uses Lisp conditions but exposes as exceptions; restart system availabler
JuliaException (hierarchy)try/catch/finally; rethrow() preserves stackexceptions cheap on JIT — used freelyjulia
CrystalExceptionbegin/rescue/ensure Ruby-styletyped rescue clauses (compile-time checked)crystal
C++any type throwable (idiom: std::exception subclass)try/catch(...)RAII handles cleanup; noexcept since C++11; exception cost on miss path significantcpp
AdaException predefined + user-definedbegin ... exception when E => ...; end;exceptions exist but contract programming (preconditions) preferredada
SmalltalkException hierarchy[...] on: Error do: [:e | ...]full resumable + outer + return + passsmalltalk
Tclerror code + resultcatch { ... } resulttry ... finally ... since 8.6tcl
MATLABMExceptiontry/catch MEMException.identifier for matchingmatlab

The cost of exception throwing in 2026:

  • Python: ~5 μs to construct + raise + catch
  • Java: ~1 μs for RuntimeException with fillInStackTrace; ~100 ns if you override fillInStackTrace
  • C++ libstdc++ Itanium ABI: ~10–100 μs (very expensive miss-path); zero-cost on no-throw path
  • Go panic + recover: ~1 μs

Exception-as-control-flow (Python’s StopIteration, Smalltalk’s resumable, R’s conditions) is fine; exception-as-error-for-every-failure-path in tight inner loops is not.

3. Checked exceptions — Java’s lonely experiment

Java’s Exception (not RuntimeException) subclasses must be declared in the throws clause and either caught or propagated by every caller. The good: every error path is visible in the API. The bad: signature churn, leaky abstractions, throws Exception catch-all, and the boilerplate that drove Kotlin/Scala/Groovy to disable checked exceptions even on the JVM.

Reaction over time:

  • 1995: Java introduces checked exceptions as a discipline.
  • 2000s: industry consensus that checked exceptions are mostly an antipattern (Hejlsberg in 2003 anti-checked interview before launching C# without them).
  • 2014: Java 8 Stream API can’t propagate checked exceptions through map/filter lambdas; workarounds proliferate.
  • 2018+: Kotlin, Scala, Groovy on JVM all skip checked.
  • 2022+: Java itself relaxes via sealed + pattern matching enabling Result-shaped types; effects (JEP 453 structured concurrency) handle propagation.

Conclusion: checked exceptions remain a Java oddity; no language since has adopted them.

4. Error-as-value (Result, Either, Maybe, Go errors)

Returning errors as ordinary values. The compiler enforces handling (in typed languages) without unwinding the stack. Trade-off: explicit at every call site (? operator in Rust hides this; if err != nil in Go does not).

LanguageTypeIdiomatic syntaxNotesLinked note
RustResult<T, E> and Option<T>? operator (try-propagate); match; let-elseFrom<E1> for E2 enables ? conversion; thiserror + anyhow crates dominate libraries vs apps; #[derive(Error)] typicalrust
Go(T, error) tupleif err != nil { return err }; no try shortcut (rejected proposal Aug 2019)errors.Is, errors.As, errors.Unwrap; %w verb in fmt.Errorf for wrapping; explicit + boringgo
HaskellEither e a, Maybe a, ExceptT e m amonadic do-notation; <- propagates Left/NothingMonadError typeclass; MonadFail for pattern-match failures; Validation for accumulatinghaskell
OCaml('a, 'b) result, 'a optionlet* x = … in with let-binding ops (4.08+); matchexceptions also exist and are used freely (Not_found, Failure); Result for API boundariesocaml
[[Languages/fsharp|F#]]Result<'T, 'E>, Option<'T>computation expression result { let! x = ... }; pipe + Result.bindC# Optional interop via FSharp.Corefsharp
ScalaEither[E, A], Try[A], Validated[E, A] (Cats)for-comprehension for { x <- ... } yield ...; pattern matchCats Effect, ZIO add typed error channelsscala
SwiftResult<Success, Failure> AND throws (both!)do/try/catch for throws; try?/try! for Optional/force-unwrap; Result for storedtyped throws since Swift 6.0 (2024) — func f() throws(MyError) { ... }swift
KotlinResult<T> (stdlib; restricted use) + nullablerunCatching { ... }.fold(success, failure)Result discouraged for public APIs; nullable + sealed classes preferredkotlin
Erlang{ok, X} / {error, Reason} tuplespattern matchthis is the canonical Erlang idiom even with try/catch availableerlang
Elixir{:ok, x} / {:error, reason} tuples; with macrocase, with, bang-functions raise (Map.fetch!):ok/:error is so canonical that with macro exists to chainelixir
GleamResult(t, e); Option(t)case; try (use op) — let x = try thing()borrowed Rust Result fully, BEAM runtimegleam
RocResult a errplatform handler boundary! operator (Roc 2024) for try-style propagationroc
Zig!T error union (MyError!T)try for propagation; catch for inline handlingerrdefer for partial-cleanup-on-error; error.X enum-likezig
Nimexceptions OR Option[T] / Result[T, E] (results lib)both availableexceptions preferred for “exceptional”; Result for “expected”nim
Odinmulti-value return: f :: proc(...) -> (T, Error)early-return idiomalso has or_return directiveodin
Mojosimilar to Rust: Result[T, E] + Python-compatible exceptionsraises annotation; try/except from Python; ! op proposedbridging Python’s exceptions with Rust’s Result is a Mojo design tensionmojo

The “Result vs exception” debate maps directly onto does the caller care which error happened. If every caller will handle every error → Result. If most callers want a generic logged-and-bubbled “something broke” → exception.

5. Optional / Maybe — the null replacement

The single biggest reduction in runtime bugs of the 2010s. Tony Hoare’s “billion-dollar mistake” (null references, ALGOL W 1965) — every language since 2010 either makes nullability explicit or types out null entirely.

LanguageTypeIdiomaticNotesLinked note
HaskellMaybe a (Just a, Nothing)do, <$>, `<>, fromMaybe`canonical
OCaml'a option (Some a, None)match, Option.bind, let* x = ...canonicalocaml
ScalaOption[A] (Some(a), None)for, .map, .flatMap, .getOrElsecanonicalscala
SwiftT? syntax sugar over Optional<T> (some, none)if let x = ..., guard let, ?? coalesce, x?.method() chainlanguage-level support is heavyswift
KotlinT? (nullable) — language-level?., ?: Elvis, !! force, let { }, smart castsnullable propagates differently than Optional (no Maybe wrapper)kotlin
RustOption<T> (Some, None)?, match, .map, .unwrap_ordistinct from Result<T, E>rust
[[Languages/fsharp|F#]]Option<'T> (Some, None)computation expression, Option.bindinherited from OCamlfsharp
[[Languages/csharp|C#]]nullable reference types (NRT) since 8.0 (2019); Nullable<T> for value typesstring?, int?, ??, ?., ! suppresspartial enforcement at compile time via #nullable enablecsharp
TypeScriptT | null | undefined (under strictNullChecks)?., ??, type narrowingTS-only; runtime has both null and undefinedtypescript
Dartsound null safety (Dart 2.12, 2021)T?, T!, late T, ??, ?.full migration from Dart 2.0 nullable everywheredart
IdrisMaybe a + dependent types let you encode non-null at type levelpattern matchprecondition encoded in typeidris
LeanOption αdo, bindtypeclass-basedlean
Coq (Rocq)option Amatch, do, option_mapproof obligations may force handlingcoq
Elm-like Roc / RocMaybe awheninherits from Elm ancestorroc

The 2018–2024 industry trend: nullable-by-default → non-null-by-default with opt-in nullability. C# (NRT 8.0), Kotlin (T? distinction from launch), Dart (sound null safety 2.12), TypeScript (strictNullChecks), even Java (proposed JEP 8303099 null-restricted types, Project Valhalla flow). The benefit is massive — Kotlin/Dart/Swift teams routinely report 90%+ reduction in null-pointer crashes in production.

6. Effect systems — algebraic effects + handlers

A research-language idea (Plotkin + Power 2003; Bauer + Pretnar 2013 Eff) that has crossed into mainstream type-driven libraries.

Language / libraryMechanismStatusNotesLinked note
Kokafirst-class algebraic effects + handlers; total inferenceresearch lang (Microsoft, Daan Leijen)model for the field; tracks every effect in typen/a
Eff / OCaml 5 effect handlersalgebraic effects (one-shot)OCaml 5.0 (2022) added effect handlers for concurrency (Eio + Domainslib)mainstream production language with effectsocaml
Effektsecond-class effect handlersresearch (Tübingen + Sussex)type-system-only effect trackingn/a
Effect-TSTypeScript librarymature; v3 released 2024Effect<R, E, A> channel — composable error channel + dependency + valuetypescript
ZIOScala librarymature; v2ZIO[R, E, A] — same shape as Effect-TS, Scala flavorscala
Cats EffectScala librarymature; v3IO[A] + tagless-final effectscala
Unisonfirst-class effects (called “abilities”)active languagemodel for distributed coden/a
Haskellmtl-style monad transformers OR polysemy OR fused-effects OR eff librarymature ecosystemtagless-final + free-monad are also alternativeshaskell

Effects unify error, state, async, logging, and resource cleanup under one type-level abstraction. The cost is steeper learning curve and slower compile times in Haskell + Scala. Effect-TS and ZIO have nudged the mainstream-TS/Scala crowd toward effect channels for new microservice code.

7. Panic, abort, supervisor restart — “let it crash”

Cultivated to extreme in Erlang/BEAM (Joe Armstrong’s “fault-tolerant systems are built by isolating crashes, not preventing them”). Errors that violate invariants are not caught — they crash the unit (process, actor, task) — and a supervisor restarts a fresh instance.

LanguageMechanismRestart authorityNotesLinked note
Erlangexit/1, error/1, throw/1; mailbox + link/monitor/supervisorOTP supervisor (one_for_one, one_for_all, rest_for_one, simple_one_for_one)the canonical formulationerlang
Elixirinherits BEAMOTP supervisor (use Supervisor, DynamicSupervisor); Task.Supervisoridentical model to Erlangelixir
Gleaminherits BEAMOTP supervisor via gleam_otp libtyped Erlang/Elixir lineagegleam
Rustpanic!() and unwrap()/expect() panic; catch_unwind for FFIthread-level abort (default) or unwind (configurable)panic is for invariant violation, not error; abort-on-panic preferred for embeddedrust
Gopanic() / recover(); goroutine-levelrecover in same goroutine; nothing restartsdiscouraged for ordinary errors — use (T, error) insteadgo
Ponyactor-level fault — ? partial functions; no exceptionsruntime catches and treats as recoverableper-actor fault containmentpony
SwiftfatalError, precondition, assertalways crashesdistinct from throwsswift
ScalaAkka actor supervisor — same model as BEAM but on JVMOneForOneStrategy, AllForOneStrategymainstream JVM port of OTP modelscala
[[Languages/csharp|C#]]Orleans actor model — same supervisor conceptOrleans grain activationMicrosoft port of OTP-ish for .NETcsharp
Cabort(), assert(), _Exit()terminates process; OS-levelexternal supervisor (systemd, daemontools, k8s liveness) onlyc

The OTP supervisor tree is the most reliable error handling pattern in production by far. WhatsApp pre-Meta ran 1.5B users on 50 engineers using Erlang OTP. Discord uses Elixir + OTP for the message bus. RabbitMQ + Riak + CouchDB all chose Erlang/OTP for this exact reason.

The supervisor pattern is incompatible with exceptions or Result as the only error-handling mechanism — supervisors require crashable units, which requires a model that says “some errors are not handled, they crash the unit on purpose”.

8. Conditions and restarts — the Lisp lineage

Common Lisp’s condition system (1989) is more powerful than every other model on this list — but its complexity prevented adoption elsewhere.

The Lisp model:

  • A condition is signaled (signal, error, warn, cerror).
  • The dynamic stack between signaler and handler is still alive — the handler can choose to (a) handle and unwind, (b) decline and let the signal continue up, (c) invoke a restart registered by the signaling site, which resumes execution at the restart point with new data.
  • The signaling site provides restarts (use-value, store-value, continue, abort); the handler picks one.

R + Dylan inherited the model. Smalltalk’s resumable exceptions are a partial port. Modern algebraic effect systems are arguably a typed re-discovery of the same idea.

LanguageCondition lineageNotesLinked note
Common Lispfull Steele 1990 condition systemerror/warn/signal + handler-bind/handler-case + restart-bind/restart-case + invoke-restartcommon-lisp
Rconditions + handlers + restartstryCatch, withCallingHandlers, withRestartsr
Smalltalkresumable exceptionsException>>resume:smalltalk
SchemeR6RS condition system; SRFI-34 + SRFI-35implementation variesscheme
Racketrich exception + delimited continuations + parameterizemost powerful Scheme on this axisracket
Clojureexceptions only; no restart systemcommunity libraries (e.g. clojure.core/ex-info) for richer infoclojure

9. Sentinel returns and shell-style exit codes

LanguageMechanismNotesLinked note
C-1, NULL, errno; setjmp/longjmp for non-localassert(3) for invariants; goto cleanup; idiomc
COBOLRETURN-CODE; condition names; declaratives sectionclassical CICS programs use abend codes + supervisor restartcobol
FortranIOSTAT, ERR= label, STAT=F2008 ERROR STOP; coarray sync errorsfortran
Bashexit code $?; set -e, trap ERR, ||, &&pipefail, errexit, nounset; pitfalls in subshell + pipelinebash
PowerShell$?, $LASTEXITCODE, try/catch; -ErrorAction Stop promotes non-terminatingnon-terminating errors are unique to PowerShell — they print + continue unless you stop thempowershell
SQLSQLSTATE codes; per-dialect raise/exceptionPL/pgSQL BEGIN ... EXCEPTION WHEN ... END; T-SQL TRY/CATCH; PL/SQL EXCEPTION WHEN ...sql
Prologthrow/1 + catch/3 + failureISO Prolog exceptions; failure-driven semantics also functions as control flowprolog
PascalI/O result variables; Delphi try/except laterclassical Pascal uses sentinel; Delphi adds OOP exceptionspascal

10. Structured concurrency and error propagation

The 2018+ shift: in a concurrent program, errors don’t belong to a single call stack — they belong to a task tree. Structured concurrency (Trio’s Nathaniel Smith, then Python asyncio.TaskGroup, Kotlin coroutines, Swift TaskGroup, Java JEP 453) enforces that child tasks are confined to a parent scope, and errors in any child propagate to the parent.

LanguagePrimitiveError propagationNotesLinked note
Pythonasyncio.TaskGroup (3.11+); Trio nurseryfirst error cancels siblings; raises ExceptionGroupPEP 654 — ExceptionGroup + except* for handlingpython
KotlincoroutineScope { launch { ... } }first failure cancels siblings; supervisorScope isolatesCoroutineExceptionHandler for unhandledkotlin
SwiftwithTaskGroup, withThrowingTaskGroupthrown error cancels grouptyped throws since 6.0swift
JavaStructuredTaskScope JEP 453 (preview J21, finalized J25)shutdownOnFailure(), shutdownOnSuccess() policiesfirst major structured-concurrency for Javajava
RustTokio JoinSet, select!, tokio::spawn — no enforced scope until v1.35 task::scope experimentmanual; JoinError returnednot enforced by language — must be designed inrust
Goerrgroup.Group (golang.org/x/sync)first error cancels via contextde facto standard but not language-levelgo
ElixirTask.Supervisor + Task.async_stream; OTPsupervisor restart strategyOTP supervisor is already structured at the actor levelelixir
ScalaCats Effect Resource / ZIO Scopetyped error channel cancels fibersfunctional-effect stylescala

11. Per-language summary table

LanguagePrimarySecondaryDefault for new codeLinked note
PythonexceptionsOptional/typingexceptions + typed Optionalpython
JavaScriptexceptionsnoneexceptions + try/catchjavascript
TypeScriptexceptions + T | nullEffect-TS (libs)strict null + exceptions; Effect for new microservicestypescript
Javaexceptions (checked + unchecked)sealed Result typesunchecked + try-with-resources + structured concurrencyjava
Csentinel + errnosetjmpsentinel + macrosc
C++exceptions OR tl::expected/std::expected (C++23)std::optionalstd::expected<T, E> for new code; exceptions still commoncpp
[[Languages/csharp|C#]]exceptionsnullable + Result libsexceptions + NRT 8.0+csharp
Goerror-as-value (T, error)panic/recover for invariantsexplicit if err != nilgo
RustResult<T, E> + ?panic for invariants? + thiserror/anyhowrust
Swiftthrows + do/try/catchResult<T, E> (stored); T?typed throws since 6.0swift
KotlinexceptionsT? + sealed Resultnullable + coroutine CoroutineExceptionHandlerkotlin
RubyexceptionsResult gemsbegin/rescue/ensureruby
PHPexceptions (PHP 7+)nonetry/catch/finallyphp
ScalaEither, Try, ZIO/Cats EffectexceptionsZIO/Cats Effect for new microservicesscala
Dartexceptionssound null safetytry/on/catch + T?dart
Elixir{:ok, x} / {:error, r} + supervisor restartexceptionsOTP supervisor + with macroelixir
HaskellEither e a, Maybe a, monad transformersruntime IO exceptionsMonadError, MonadFail, MTL or effectshaskell
Clojureexceptionsex-info dataexceptions + ex-info mapsclojure
[[Languages/fsharp|F#]]Result<'T, 'E>, Option<'T>exceptionscomputation expressionsfsharp
Luapcall/xpcallsentinel returnslocal ok, err = pcall(...)lua
Rconditions + restartstryCatchtryCatch + withCallingHandlers + condition objectsr
Juliaexceptionsnoneexceptions; cheap on JITjulia
Zigerror union !Terrdefertry + errdeferzig
Nimexceptions OR Result (results lib)bothexceptions default; Result optionalnim
Crystalexceptionsnonebegin/rescue/ensurecrystal
OCamlexceptions + result + effects (OCaml 5)both equally idiomaticexceptions for fast path, result for API boundary, effects for concurrencyocaml
Perldie / $@ / Try::Tinynonecore try/catch since 5.34perl
Erlangexit/error/throw + supervisor{ok, _} / {error, _} tuplesOTP supervisor + tupleserlang
Racketexceptions + delimited continuationsconditionswith-handlersracket
Common Lispconditions + restartsnone neededhandler-bind/case + restart-casecommon-lisp
Schemeconditions (R6RS) + SRFI-34/35impl-specificwith-exception-handlerscheme
FortranIOSTAT + ERR/STATF2008 ERROR STOPIOSTAT + STATfortran
COBOLRETURN-CODE + DECLARATIVES + CICS abendnonesentinel + declarativescobol
Adaexceptions + pragma Restrictions + SPARK contractsnoneexceptions; contracts in SPARKada
PascalI/O result; Delphi try/exceptnonevariespascal
Prologthrow/catch + failurenonecatch/3prolog
Tclcatch + try/finallynonetry/finally (8.6+)tcl
Groovyexceptions (Java)nonetry/catch/finally; no checkedgroovy
Bashexit code + trap ERRnoneset -euo pipefail + trapbash
PowerShellnon-terminating + terminating; try/catch-ErrorAction Stoptry/catch + -ErrorActionpowershell
SQLSQLSTATE + per-dialectnonePL/pgSQL EXCEPTION blocksql
Voption ? + result !exceptions alsooption + result preferredv
Odinmulti-value return + or_returnnonemulti-valueodin
RocResult a errnoneplatform-level error channelroc
GleamResult(t, e) + trysupervisor restart on BEAMResult + OTPgleam
Pony? partial functionsactor-level faultpartial typingpony
IdrisMaybe + dependent preconditioneffect handler libsencode in typeidris
LeanOption, Except, IO.Erroreffect-like monadsmonadiclean
Coq (Rocq)option, sumproof obligationtype-encodedcoq
AgdaMaybe, Either, typed totalitytype-encodedagda
Smalltalkresumable exceptionsnoneon:do:; resume/return/passsmalltalk
MATLABMException + try/catchnonetry/catch MEmatlab
Mojoraises + Python try/exceptResult-style emergingraises annotationmojo

12. The 2024–2026 convergence

Several distinct lineages are converging on a typed two-channel model: one channel for the happy-path value, one channel for typed errors, with cheap propagation.

  • Swift 6.0 (2024): typed throws — func f() throws(MyError) { ... } — same signature shape as Rust Result<T, MyError> but with throw syntax.
  • Java JEP 453 (J21 preview, J25 final): StructuredTaskScope + sealed Result types via pattern matching.
  • TypeScript: Effect-TS v3 normalizes Effect<R, E, A> channel for libraries; works around TypeScript’s untyped catch.
  • Scala 3: union types + sealed Result; ZIO 2.x + Cats Effect 3.x are the mainstream paths.
  • Python 3.11+ ExceptionGroup + except* — handles parallel errors first-class.
  • Kotlin 2.x: arrow-kt brings Either/Validated to Kotlin idiomatically; structured concurrency error model matured in coroutines 1.7+.
  • Rust 2024 edition + try blocks (still nightly) + Try trait stabilization (in progress).
  • C++23: std::expected<T, E> standardized — formalizes a decades-old library pattern.
  • Mojo: tries to reconcile Python exceptions + Rust Result at the same site.

The net direction: typed errors at API boundaries, panics for invariants, supervisors for crash containment in concurrent code, structured concurrency to scope error propagation. The exception-vs-Result war is over — they’re complements at different layers.

Adjacent

When to pick what

What's the application?
├─ Library / API for external consumers
│   ├─ Statically-typed lang → typed Result/Either (Rust, OCaml, Haskell, F#) or typed throws (Swift 6+)
│   └─ Dynamically-typed lang → exceptions with rich exception classes (Python, Ruby)
├─ Application internal code
│   ├─ Single thread of control → exceptions are fine; just catch at boundaries
│   └─ Concurrent → structured concurrency + typed errors propagated through task group
├─ Concurrent / distributed service with millions of connections
│   └─ Actor + supervisor (Erlang/Elixir OTP, Akka, Pony, Orleans) — let-it-crash + restart
├─ Embedded / safety-critical
│   ├─ No allocation, no unwind → C with sentinel + assert + watchdog; or Rust with panic=abort + Result + #![no_std]
│   ├─ Formal verified → SPARK Ada, Coq, Agda, Lean
│   └─ Hard real-time → C with errno; or Rust with no panic in core
├─ Web frontend (browser)
│   └─ try/catch at outer boundaries + Error Boundary (React) + global unhandled-rejection handler; Effect-TS for state machines
├─ CLI tool / script
│   ├─ Short-lived → exceptions fine; let the runtime print stack
│   ├─ Bash → `set -euo pipefail` + trap ERR
│   └─ PowerShell → -ErrorAction Stop + try/catch
├─ Long-running daemon
│   ├─ Erlang/Elixir → OTP supervisor (best)
│   ├─ Rust/Go → top-level error log + restart by systemd/k8s; Result/error propagation in code
│   └─ JVM → top-level uncaught handler + crash + k8s liveness probe
├─ Database / data pipeline
│   ├─ PL/pgSQL → EXCEPTION blocks with SQLSTATE pattern
│   ├─ Python ETL → exceptions with retries + dead-letter queue + structured logging
│   └─ Spark/Flink → checkpointing + idempotent transformations
├─ Functional core / type-driven code
│   ├─ Haskell → `Either e a`, `ExceptT`, mtl or effects (polysemy/eff)
│   ├─ Scala → ZIO / Cats Effect
│   ├─ F# → Result + computation expression
│   ├─ TypeScript → Effect-TS
│   └─ OCaml 5 → effects + `result`
└─ Scientific / numerical code
     ├─ MATLAB → MException + try/catch ME
     ├─ Julia → exceptions are cheap
     ├─ Python (NumPy) → catch ArithmeticError, ValueError; numerically-stable code where possible
     └─ R → tryCatch + withCallingHandlers + conditions

The biggest practical lesson 2015–2026: stop arguing about exception vs Result and pick the model that matches your concurrency story. Single-thread + few errors → exceptions. Concurrent + tree-of-tasks + need-to-cancel-siblings → structured concurrency with typed errors. Distributed + fault-tolerant → supervisor restart. Library boundary → typed Result regardless of internal style. The wrong model for the wrong layer is what bakes pain into the codebase; the right model is the one that lets the next maintainer reason about every failure path in five minutes.