Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/Guide/ERRORS.md
blob: 20c524fc05822964c00ea5ba09b2af86d4e79c42 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> -->
<!-- SPDX-License-Identifier: CC-BY-4.0 -->

# 7. Errors

A failure is one of three things, and each has one way of being handled.

| Kind | Example | Handled by |
| --- | --- | --- |
| **Recoverable failure** | a malformed input file, a missing path, a value out of range | returning `Expected<T>` or `Error` |
| **Broken invariant** | a verifier that accepted an IR it must reject, an index the caller guaranteed in range | an assertion, or a fatal error in release builds |
| **Unrecoverable environment** | out of memory, a corrupted heap | termination |

## 7.1 No exceptions in libraries and tools

Code is compiled with `-fno-exceptions` (`/clang:-fno-exceptions` with `clang-cl`). There is no `throw`, `try` or
`catch` in first-party code, including tests, fuzzers and benchmarks.

The reasons are practical: an exception is invisible in a signature, so a caller cannot see that a call can fail; it
cannot cross a C interface; and its cost and its interaction with destructors are hard to reason about in code that also
runs under sanitizers and fuzzers.

**A project may opt in** when its host model depends on exceptions, as a C++ test framework that reports a failed
assertion by throwing does. The project says so in its documentation, keeps exceptions inside its own code, and still
never lets one cross a C interface or escape a destructor. Third-party code keeps its own compiler options either way.

## 7.2 `Expected<T>` and `Error`

A function that produces a value or fails returns `Expected<T>`; one that only succeeds or fails returns `Error`. The guide
does not prescribe the library. `llvm::Expected<T>` and `llvm::Error` suit code that already depends on LLVM; a
backport of `std::expected` (such as `tl::expected`) or a small type of the project's own suit the rest, and C++23's
`std::expected` replaces them once a project moves to it. What matters is the discipline below: the failure is in the
signature, it cannot be ignored, and it carries a message.

The examples use LLVM's types.

```cpp
[[nodiscard]] auto
ParseHeader(llvm::ArrayRef<std::uint8_t> bytes) -> llvm::Expected<Header>
{
    if (bytes.size() < kHeaderSize)
    {
        return llvm::createStringError(
            "header is %zu bytes; at least %zu are required",
            bytes.size(),
            kHeaderSize);
    }
    // ...
}

[[nodiscard]] auto
LoadModule(const std::filesystem::path &path) -> llvm::Expected<Module>
{
    auto bytes = ReadFile(path);
    if (!bytes)
    {
        return bytes.takeError();
    }
    auto header = ParseHeader(*bytes);
    if (!header)
    {
        // path::c_str() is wide on Windows; string() is the narrow form.
        return llvm::joinErrors(
            llvm::createStringError("%s: not a module",
                                    path.string().c_str()),
            header.takeError());
    }
    return Module{*header, std::move(*bytes)};
}
```

- **Every result is inspected exactly once.** Prefer a type that enforces this: an `llvm::Error` that is destroyed
  unchecked aborts a debug build, and `[[nodiscard]]` catches the rest. Propagate the error, handle it, or consume
  it with a comment saying why it may be dropped.
- **Add context on the way up**, so the final message says what was being done: the file, the record, the field. Do not
  wrap an error without adding information.
- **Messages are lower-case, without a trailing period**, and name the offending value: `entry 'Demo.Main' names no
  class`. They are written for the person who has to fix the input.
- **A typed error** (an error class or an error enumeration, such as a class derived from `llvm::ErrorInfo`) when a
  caller needs to react to one kind differently, for example to retry. Otherwise an error with a message is enough.
- **Do not use `std::error_code` as the interface** of new code. At the boundary of an API that reports through
  `std::error_code`, such as the non-throwing `std::filesystem` overloads, convert it into the project's error
  type (`llvm::errorCodeToError` with LLVM).
  Always call the non-throwing overload; code built without exceptions cannot catch the other one.

## 7.3 Invariants

An invariant is something the program's own logic guarantees. A broken invariant is a bug in the program, not in its
input, and there is no meaningful way to continue.

- **`assert`** checks an invariant that is cheap to state, in debug builds. It never checks input, never has a side
  effect, and its message is a condition a reader understands: `assert(index < blocks_.size() && "block id out of range")`.
- **An always-on check** (a project macro that prints and calls `std::abort`, or `llvm::report_fatal_error`) guards
  an invariant whose
  violation would corrupt memory or produce wrong output in a release build, such as a verifier disagreeing with
  itself. It prints a message and terminates.
- **An unchecked unwrap** (`llvm::cantFail`, or `.value()` behind an assertion) takes the value of an `Expected` whose
  success the caller has just established, such as an
  operation on two sets that were created with the same size a line above. It is never a shortcut for input
  validation.
- **An unreachable marker** (a project macro, `llvm_unreachable`, or C++23's `std::unreachable` behind an assertion)
  follows a `switch` that handles every enumerator; it is not a way to silence a missing case.

The line between the two first kinds is the source of the value: **anything that comes from outside the process**
(a file, the network, the command line, the environment, another component's serialized output) is validated and
reported as a recoverable failure, never asserted.

## 7.4 Fatal defects in tests and fuzzers

A fuzz oracle that detects a defect, such as a round trip that changes a value, terminates with a fatal error so that
the fuzzer keeps the input. A benchmark whose fixture is corrupt terminates as well; it must not time a broken
workload. A test that cannot set up its fixture fails with a message, not a crash.

## 7.5 Destructors and cleanup

A destructor never fails. Cleanup that can fail, such as flushing a file, is a member function that returns `Error`
and that the owner calls before the object goes away. The destructor then only releases what remains, silently.

## 7.6 Diagnostics are not errors

A compiler or a linter that reports problems in its input produces **diagnostics**: data with a severity, a location
and a message, collected and printed by a sink. Producing a diagnostic is normal operation and does not use
`Expected`. `Expected` is for the failure of an operation itself, such as being unable to read the input at all.