# 1.6. Error Handling
## 1.6.1. One error type
RustyML has exactly one error type, `rustyml::error::Error`. Every fallible operation in the crate returns `Result<T, rustyml::error::Error>`, which also has an alias:
```rust,ignore
pub type RustymlResult<T> = std::result::Result<T, Error>;
```
`Error` is not in the `prelude`, so the error machinery is imported separately with `use rustyml::error::Error;` and friends. These are `Error`'s variants:
| Variant | Trigger | `Display` message (`{}` / `to_string()`) |
|---|---|---|
| `EmptyInput(String)` | An array, vector, or dataset was empty where data was required | `input is empty: <what>` |
| `DimensionMismatch { expected, found }` | Two scalar counts disagreed | `dimension mismatch: expected <e>, found <f>` |
| `ShapeMismatch { expected, found }` | Two tensor shapes disagreed (a gradient vs. the activation it flows into) | `shape mismatch: expected [..], found [..]` |
| `NonFinite(String)` | A value in the data or produced by a computation was `NaN` / `inf` | `non-finite value (NaN or infinity) encountered in <where>` |
| `InvalidParameter { name, reason }` | A user-supplied hyperparameter was out of range | ``invalid parameter `<name>`: <reason>`` |
| `InvalidInput(String)` | A validation failure with no more specific variant (bad rank, too few samples) | `invalid input: <msg>` |
| `NotFitted(&'static str)` | A method needing a trained model was called before `fit` | ``model `<name>` has not been fitted; call `fit` before this operation`` |
| `NotConverged(String)` | An iterative algorithm never met its convergence criterion | `failed to converge: <msg>` |
| `Computation { context, source }` | A numerical breakdown, a violated invariant, or a wrapped foreign error | `computation failed: <context>` |
| `NeuralNetwork(NnError)` | A neural-network-specific failure | forwarded transparently from `NnError` |
| `Tree(TreeError)` | A decision-tree-specific failure | forwarded transparently from `TreeError` |
| `Io(IoError)` | A filesystem or (de)serialization failure | forwarded transparently from `IoError` |
Note that `DimensionMismatch` compares scalar counts, such as a feature count or a vector length, while `ShapeMismatch` is about two whole tensor shapes disagreeing, which shows up mostly in the neural-network code.
`Error` is annotated `#[non_exhaustive]`, which means a `match` over it **must** carry a wildcard `_ =>` (or `Err(e) =>`) arm.
## 1.6.2. The domain sub-errors
Three of `Error`'s variants each wrap a smaller enum. Concerns that only apply to neural networks (layer state, weight shapes, compilation) and those that only apply to trees (classification versus regression) stay in their own enum.
**`NnError`** (at `rustyml::neural_network::NnError`) contains:
- `ForwardPassNotRun(&'static str)`
- `WeightShape { name, expected, found }`
- `NotCompiled(&'static str)`
- `EmptyModel`
Code example:
```rust
use rustyml::neural_network::sequential::Sequential;
use rustyml::neural_network::layers::Dense;
use rustyml::neural_network::layers::activation::ReLU;
use rustyml::neural_network::NnError;
use rustyml::error::Error;
use ndarray::Array;
fn main() {
let mut model = Sequential::new();
model.add(Dense::new(4, 2, ReLU::new()).unwrap());
let x = Array::ones((3, 4)).into_dyn();
let y = Array::ones((3, 2)).into_dyn();
// compile() was never called, so no optimizer or loss is configured yet
match model.fit(&x, &y, 1) {
Ok(_) => unreachable!("training should not have started"),
Err(Error::NeuralNetwork(NnError::NotCompiled(missing))) => {
println!("compile the model first: `{missing}` is not specified");
}
Err(e) => println!("unexpected: {e}"),
}
}
```
**`TreeError`** (at `rustyml::machine_learning::TreeError`) has these two variants:
- `NotClassificationTree`
- `CorruptStructure(&'static str)`
Code example:
```rust
use rustyml::machine_learning::{Algorithm, DecisionTree, TreeError};
use rustyml::error::Error;
use ndarray::array;
fn main() {
// A regression tree (is_classifier = false) has no per-class probabilities
let tree = DecisionTree::new(Algorithm::CART, false).unwrap();
let x = array![[1.0, 2.0]];
match tree.predict_proba(&x) {
Err(Error::Tree(TreeError::NotClassificationTree)) => {
println!("predict_proba is classification-only");
}
other => println!("unexpected: {other:?}"),
}
}
```
**`IoError`** (at `rustyml::error::IoError`) has four variants:
- `Std(std::io::Error)` for filesystem failures
- `Serialization(postcard::Error)` for the binary format (RustyML serializes with [postcard](https://docs.rs/postcard))
- `ModelStructureMismatch(String)` for when a loaded neural-network file does not match the target architecture (a different number of layers, a different layer type at some position, or a weight whose shape does not fit the target layer)
- `UnsupportedModelFormat(String)` for when the file is not a RustyML model file at all, or its on-disk format version is not the one this build writes
Code example:
```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::error::{Error, IoError};
fn main() {
match LinearRegression::load_from_path("model_that_does_not_exist.bin") {
Ok(_) => unreachable!("the file should not exist"),
Err(Error::Io(IoError::Std(io_err))) => {
// io_err is the underlying std::io::Error (kind NotFound here).
println!("filesystem error: {io_err}");
}
Err(Error::Io(IoError::Serialization(e))) => {
println!("the file exists but is not a valid model: {e}");
}
Err(e) => println!("unexpected: {e}"),
}
}
```
For the serialization format and versioning, see [7.2. Model Persistence in Depth](../Chapter-07/7.2._Model_Persistence_in_Depth.md).
## 1.6.3. Matching on specific variants
The everyday failure is calling `predict` before `fit`, which returns `Error::NotFitted` carrying its own name as a `&'static str`:
```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::error::Error;
use ndarray::array;
fn main() {
// Constructed, but never fitted
let model = LinearRegression::new(true);
let x = array![[1.0, 2.0], [3.0, 4.0]];
match model.predict(&x) {
Ok(preds) => println!("{preds:?}"),
Err(Error::NotFitted(name)) => {
println!("`{name}` was not fitted; call fit() first");
}
Err(Error::DimensionMismatch { expected, found }) => {
println!("wrong feature count: model wants {expected}, got {found}");
}
// `Error` is `#[non_exhaustive]`, so the wildcard arm is mandatory
Err(e) => println!("other error: {e}"),
}
}
```
The `DimensionMismatch` arm is there to show the pattern; this particular call actually triggers `NotFitted`. But feed a fitted model a matrix with the wrong number of columns and you take the second arm, with `expected` set to the feature count seen at `fit` time and `found` set to the one you passed to `predict`.
## 1.6.4. Propagating with `?`
The whole crate uses a single error type, so failures anywhere in a pipeline can be returned as `Error` with nothing beyond `Result` and `?`:
```rust
use rustyml::machine_learning::{LinearRegression, RegularizationType};
use rustyml::error::RustymlResult;
use ndarray::{array, Array1, Array2};
fn train_and_predict(x: &Array2<f64>, y: &Array1<f64>) -> RustymlResult<Array1<f64>> {
// Every ? below lifts a rustyml::error::Error out of a fallible call
let mut model = LinearRegression::new(true)
.with_regularization(RegularizationType::L2(0.01))?; // maybe InvalidParameter
model.fit(x, y)?; // maybe EmptyInput / DimensionMismatch / NonFinite
let preds = model.predict(x)?; // maybe NotFitted / DimensionMismatch
Ok(preds)
}
fn main() {
let x = array![[1.0], [2.0], [3.0]];
let y = Array1::from_vec(vec![2.0, 4.0, 6.0]);
match train_and_predict(&x, &y) {
Ok(preds) => println!("got {} predictions", preds.len()),
Err(e) => eprintln!("pipeline failed: {e}"),
}
}
```
When you do need to report a foreign error (from the standard library or another crate) while folding it into this scheme and keeping its cause chain, reach for the `Context` extension trait (which has to be imported into scope). It is implemented for any `Result<T, E>` whose `E` is `Send + Sync + 'static` and implements `std::error::Error`, so it composes with `?`. `context` takes the message eagerly, while `with_context` takes a closure that runs only on the error path — prefer the closure form whenever building the message allocates (anything with `format!`), so the success path never runs it:
```rust
use rustyml::error::{Context, Error, RustymlResult};
fn parse_threshold(raw: &str) -> RustymlResult<f64> {
// A std ParseFloatError, wrapped together with our context as Error::Computation,
// with its source() chain preserved for downcasting later.
let value: f64 = raw
.parse()
.with_context(|| format!("parsing threshold from {raw:?}"))?;
Ok(value)
}
fn main() {
match parse_threshold("not-a-number") {
Ok(v) => println!("threshold = {v}"),
Err(Error::Computation { context, source }) => {
println!("{context}");
if let Some(cause) = source {
println!(" caused by: {cause}");
}
}
Err(e) => println!("unexpected: {e}"),
}
}
```
The foreign error becomes the `source` of an `Error::Computation`, reachable through the standard `std::error::Error::source()` chain and downcastable back to its original concrete type without losing any information.
## 1.6.5. Eager validation
RustyML's error-handling design is that anything taking a hyperparameter validates it eagerly and returns `Result`, rather than panicking on illegal input.
```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::machine_learning::linear_model::LeastSquaresSolver;
use rustyml::error::Error;
fn main() {
// learning_rate must be positive and finite
// 0.0 returns an error
match LinearRegression::new(true).with_solver(LeastSquaresSolver::GradientDescent {
learning_rate: 0.0,
max_iter: 1000,
tol: 1e-6,
}) {
Ok(_) => unreachable!("a zero learning rate must not be accepted"),
Err(Error::InvalidParameter { name, reason }) => {
// bad parameter `learning_rate`: must be positive and finite, got 0
println!("bad parameter `{name}`: {reason}");
}
Err(e) => println!("unexpected: {e}"),
}
}
```
A few places still panic outright:
- The functions in the `metrics` and `math` modules panic on an error instead of returning `Result`, which keeps those modules lightweight.
- Whether an `ndarray` operation outside RustyML returns `Result` or panics is `ndarray`'s decision, and RustyML has no say in it.