現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【C++】Why Are Exceptions Often Prohibited?
As you learn C++, you will eventually come across exceptions.
Exceptions are a wonderful way to handle errors, but they are frequently prohibited.
When using C++ for enterprise software, educational software, web software, etc., exceptions are strongly recommended.
However, in fields such as game development, finance, AI, automotive, robotics, etc., exceptions are usually almost completely banned.
Why is that?
Let me explain.
My Position
Personally, I have mixed feelings about exceptions.
They are extremely useful for debugging, but I prefer to avoid them in production environments.
I recently released Shader Playground with exception handling, but since it is an educational tool, there is no problem.
What Are Exceptions?
To be honest, C++ errors are often very difficult to understand, so debugging what happened when a program crashes is really tough.
Using exceptions prevents crashes, allows you to throw an error, and lets the program continue as if nothing happened (if it is not a critical part of the code).
You can also return human-readable errors.
Here is an example of fictional C++ code:
#include <iostream>
#include <stdexcept>
#include <engine/gfx/vulkan.hh>
void InitVulkan() {
engine::gfx::Vulkan vk;
if (vk.IsError()) throw std::runtime_error(vk.ErrorMsg());
}
int Add(int a, int b, int expect) {
int sum = a + b;
if (sum != expect)
throw std::logic_error("計算結果が期待値と一致していません: "
+ std::to_string(sum) + " != "
+ std::to_string(expect));
return sum;
}
int main() {
try {
InitVulkan();
Add(2, 2, 5);
} catch (const std::runtime_error &e) {
std::cout << e.what() << std::endl;
return 1;
} catch (const std::logic_error &e) {
std::cout << e.what() << std::endl;
} catch (...) {
std::cout << "不明なエラー" << std::endl;
return 1;
}
std::cout << "Vulkan初期設定完了!" << std::endl;
return 0;
}
In this example, Vulkan is initialized and 2 + 2 is calculated expecting the result to be 5.
Vulkan initialization may fail if the GPU is unsupported or the Vulkan library (.dll or .so) does not exist.
However, the addition function will always fail.
Because 2 + 2 = 4, not 5.
What is thrown depends on what fails first.
If Vulkan initialization fails before the addition, it becomes a runtime error.
If it succeeds, it becomes a logic error.
Also, notice that return 1; is omitted in the logic error section.
This is because whether 2+2 is 5 or 4 is not important for Vulkan initialization, so it throws an error while allowing the program to continue execution.
It is intentionally included to demonstrate the benefits of exceptions.
Also, you can see catch (...).
This does not mean "put exception handling here", it is actually valid C++ code.
It means "any other thrown error", which does not occur in this case, but can sufficiently happen in large codebases.
The Cost of Exceptions
This is the main reason exceptions are prohibited: they significantly degrade performance.
Other reasons include integration with C libraries (multiple error handling styles) and increased complexity.
But the real reason exceptions are on the "terrorist watch list" is the performance cost.
Every time an error occurs (more frequently than imagined in large codebases), memory allocation happens during throw, the stack is unwound, objects live a bit longer, and Run-Time Type Information is required.
All of these consume CPU cycles and memory space, which are extremely valuable resources in fields where performance is critical.
In games, it means lower frame rates (FPS).
In finance, it can mean huge losses due to timing degradation.
In medicine, it can mean death because an electric shock is not delivered at the exact timing.
In automobiles, it means potential collision accidents.
In IoT devices, firmware collapses due to insufficient CPU power and memory.
When developing for game consoles like Switch or PS5, the SDK intentionally does not support exceptions, so using exceptions causes compilation errors.
Therefore, in companies making games for these systems, exceptions are not only prohibited but technically impossible.
On the other hand, in enterprise, web, education, and scientific software, exceptions tend to be preferred.
Because performance is rarely an issue in these fields.
Instead, security and memory safety are emphasized.
However, there is a catch: C++ is rarely used in these fields.
Enterprises prefer C# or Java, education and science prefer Python, and web prefers PHP, Javascript, Go, Ruby, Rust, or whatever language is trending at the time.
In my case, I use C++ both for my dayjob and my own company.
My company makes games for Nintendo Switch and Switch 2, so we do not use exceptions at all.
In my dayjob, I use exceptions heavily in calculation-heavy construction tools, but avoid them in LiDAR-related software.
In short, it depends on the requirements.
Both jobs are bound by strict NDAs, so I can only talk this much.
Alternatives
Assert
Fortunately, there are alternatives.
The most common is to combine enum representing the result with assert.
Instead of human-readable error messages, you get error codes.
You can also attach messages to assert.
The great thing about assert is that it only asserts and aborts in debug mode, and does nothing in release mode.
This may be a new concept for hobbyist C/C++ developers, but distinguishing between debug and release modes is standard in professional environments.
Simply put, debug mode turns off all compiler optimizations, keeps labels, and enables debug tools.
Release mode maximizes optimizations, removes all debug tools, and optionally strips labels to reduce file size.
Also, it is standard that the DEBUG macro is defined in debug mode and the NDEBUG macro in release mode.
Super Mario 64 ran in debug mode on all Japanese and US Nintendo 64 hardware, but the European version noticed this mistake and changed to a release build.
As a result, Dire Dire Docks had significant lag in Japanese and US versions, but not in the European version.
One example:
#include <cassert>
#include <iostream>
#include <engine/gfx/vulkan.hh>
int main() {
engine::gfx::Vulkan vk;
assert(!vk.IsError() && vk.ErrorMsg().c_str());
int sum = 2 + 2;
assert(sum == 4 && "計算結果が期待値と一致していません。");
std::cout << "Vulkan初期設定完了!" << std::endl;
return 0;
}
The problem with the above code is that it only aborts in debug mode.
To fix this, do the following:
#include <cassert>
#include <iostream>
#include <engine/gfx/vulkan.hh>
int main() {
engine::gfx::Vulkan vk;
if (vk.IsError()) {
assert(false && vk.ErrorMsg().c_str());
return -1;
}
int sum = 2 + 2;
assert(sum == 4 && "計算結果が期待値と一致していません。");
std::cout << "Vulkan初期設定完了!" << std::endl;
return 0;
}
This reproduces the try-catch version where Vulkan initialization failure stops execution, but 2 + 2 not equaling 4 continues execution even in release mode.
Since assert comes from the C standard library, when using C instead of C++, the header becomes assert.h instead of cassert, and it works the same.
Because the string becomes const char * type, and vk.ErrorMsg() returns std::string, .c_str() was needed.
In C language:
#include <assert.h>
#include <stdio.h>
#include <engine/gfx/vulkan.h>
int main() {
if (vk_init() != 0) {
assert(false && vk_get_error());
return -1;
}
int sum = 2 + 2;
assert(sum == 4 && "計算結果が期待値と一致していません。");
puts("Vulkan初期設定完了!");
return 0;
}
Result
Another common method is to use a Result type (you need to create it yourself).
I first encountered it when making games for Nintendo hardware, found it very convenient, reproduced it in PHP, and now I can't live without it.
The Result type is like an enum starting with ResultOK, enumerating all edge cases, assigning each error type, and generating error messages from there.
I made it even as a class (or struct) with Success and Error methods.
Both methods take a message string and a data array.
In the Error method, only the message string is required, other parameters are optional.
PHP version (requires PHP 8.0 or higher):
class Result {
public bool $isSuccess;
public ?string $message;
public array $data;
public function __construct(bool $isSuccess, ?string $message = null, array $data = []) {
$this->isSuccess = $isSuccess;
$this->message = $message;
$this->data = $data;
}
public static function Success(?string $message = null, array $data = []): self {
return new self(true, $message, $data);
}
public static function Error(string $message, array $data = []): self {
return new self(false, $message, $data);
}
}
function Get(): Result {
return Result::Success("成功", [20, "詳細"]);
}
Equivalent implementation in C++ (requires C++17 or higher):
#include <string>
#include <vector>
#include <any>
class Result {
public:
bool isSuccess = false;
std::string message = "";
std::vector<std::any> data{};
Result(bool isSuccess, std::string message = "", std::vector<std::any> data = {})
: isSuccess(isSuccess)
, message(std::move(message))
, data(std::move(data)) {}
static Result Success(std::string message = "", std::vector<std::any> = {}) {
return Result(true, std::move(message), std::move(data));
}
static Result Error(std::string message, std::vector<std::any> = {}) {
return Result(false, std::move(message), std::move(data));
}
};
Result Get() {
return Result::Success("成功", {20, "詳細"});
}
std::expected
In C++23, std::expected was introduced.
This is a new way to handle errors without sacrificing performance or production reliability.
It behaves like assert but works in release mode too.
Example:
#include <print>
#include <expected>
#include <string>
std::expected<int, std::string> Add(int a, int b, int expected) {
int sum = a + b;
if (sum != expected) return std::unexpected("計算結果が期待値と一致していません。");
return sum;
}
int main() {
auto res = Add(2, 2, 5);
// auto res = Add(2, 2, 4);
if (res) std::println("結果:{}", *res);
else std::println("エラー:{}", res.error());
return 0;
}
Or:
int main() {
auto res = Add(2, 2, 5)
// auto res = Add(2, 2, 4)
.transform([](const auto &data) {
return data;
})
.or_else([](const auto &err) {
std::println("エラー:{}", err);
return -1;
});
std::println("結果:{}", *res);
return 0;
}
This reminds me of Go's error type:
package main
import (
"fmt"
"errors"
)
func Add(a, b, expected int64) (int64, error) {
sum := a + b
if sum != expected {
return 0, errors.New("計算結果が期待値と一致していません。")
}
return sum, nil
}
func main() {
sum, err := Add(2, 2, 5)
// sum, err := Add(2, 2, 4)
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Println("結果:", sum)
}
}
That's all