Skip to content

Exceptions

Joël R. Langlois edited this page Jan 12, 2026 · 7 revisions

Don't Use Exceptions, No Exceptions

C and C++ exceptions are prohibited in this codebase.

This is a deliberate, permanent design choice based on clarity, predictability, and maintainability.

Exceptions introduce hidden control flow, implicit contracts, and non‑local side effects that make code harder to reason about, harder to debug, and harder to guarantee across platforms, compilers, and build configurations. They also impose runtime and binary‑size costs that are unacceptable in performance‑sensitive or real‑time environments.

By removing exceptions entirely, we ensure that:

  • all control flow is explicit
  • all error paths are visible at the call site
  • all contracts are documented and enforced intentionally
  • behaviour is consistent across compilers, platforms, and optimisation levels
  • debugging never involves invisible stack‑unwinding or half‑executed states

Codebases are to use explicit return types, status objects, and well‑defined error‑handling patterns. These mechanisms make failure modes predictable, testable, and easy to follow - without relying on hidden language machinery.

This is not a stylistic preference. This is a foundational architectural rule.

Exceptions should ideally be disabled, marked unsupported, and generally not permitted under any circumstances (outside of required dealings with external libraries and native code).

Recommendations

Since exceptions are not permitted, all error handling must be explicit, visible at the call site, and part of the function’s contract. The following patterns are approved and encouraged.

Use return types that encode success or failure

struct Status
{
    bool ok = true;
    std::string message;
};

Status loadConfig (const juce::File& file); 

...or even better with JUCE, use juce::Result:

juce::Result loadConfig (const juce::File&); 

Usage:

if (const auto result = loadConfig (file); status.failed())
{
    logError (status.getErrorMessage()); 
    return;
} 

Use std::optional when a value may or may not exist

std::optional<User> findUser (UserID);

Usage:

if (auto user = findUser (id))
    process (*user);
else
    handleMissingUser (id);

Use std::expected (or a similar type) for value-or-error results

using LoadResult = std::expected<Config, std::string>;

LoadResult loadConfig(const File& file);

Usage:

auto result = loadConfig(file);

if (! result)
{
    logError(result.error());
    return;
}

applyConfig(*result);

Use error enumerations for well-defined failure modes

enum class ParseError
{
    none,
    invalidFormat,
    missingField,
    outOfRange
};

ParseError parse (const std::string& text, Data& out);

Usage:

bool doImportantStuff (const std::string& text, DataType& dataOut)
{
    switch (parse (text, dataOut))
    {
        case ParseError::InvalidFormat: return handleInvalidFormat();
        case ParseError::MissingField:  return handleMissingField();
        case ParseError::OutOfRange:    return handleOutOfRange();

        case ParseError::None: break;
        default: jassertfalse; break;
    }

    return true;
}

Use assertions for programmer errors, not runtime errors.

void myFunc (...)
{
    jassert (buffer != nullptr);
    jassert (size > 0);

}

Clone this wiki locally