Which errors in computer programs do you know? In small groups: Collect as many as you can, then try to group them into categories!
| Model | Representation | Signaling | Propagation | Handling | Languages |
|---|---|---|---|---|---|
| Error Codes | Primitive Types (e.g. int), Composites, Interfaces |
Return Value | Return Value | err == ERROR_NUMBER err != nil |
C C++ Go |
| Software Exception | Exception (Sub-)Class, Primitives, Composites |
Method Signature, Documentation |
Non-local jumps | try/catch blocks | C++ Java Python |
| Result Types | Variant / Tagged Union | Return Value | Return Value | pattern matching | Rust Zig Haskell |
int read_file(const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) { // handle other error
fprintf(stderr, "Error opening file '%s': %s\n",
filename, strerror(errno));
return -1; // propagate error
}
printf("File '%s' opened successfully.\n", filename);
fclose(fp);
return 0; // success
}nullptr on a nullable type (e.g. FILE*) or simple integersint a regular return value or an error?int and and error code?return ERROR_NUMBER;
fopen returns FILE*, read_file returns int, how to combine?fp == NULL, err != OKFILE* vs. int)void read_file(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) { // handle other error(-ish)
// propagate
throw std::runtime_error("Error opening file: " + filename);
}
std::cout << "File '" << filename << "' opened successfully.\n";
}
int main(int argc, char** argv) {
try {
read_file(argv[1]);
} catch(const std::runtime_error& e) { // handle exception
std::cerr << "Runtime error: " << e.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}std::exceptionnoexcept in C++catch block in the call stack
catch block?catch blockcatch due to type matchingnoexcept!)
throws IOException)std::vector::push_back)?drop the VecOption<T>?fn log(num: i32) -> Option<i32>fn read_file(file: &str) -> Option<Vec<u8>>Option<T>Option<T> does not contain information about the nature of the errorResult<T, E>Option<T>, but with an extra error type EE can be anything: String, error code, complex error structureResult<T, E> usage examplefn read_file(file_path: &Path) -> Result<Vec<u8>, String> {
match std::fs::read(file_path) { // Error handling
Ok(bytes) => Ok(bytes),
Err(why) => // Propagation
Err(format!("Could not read file ({})", why)),
}
}
// The same as:
std::fs::read(file_path)
.map_err(|why| format!("Could not read file({})", why))fn sum_numbers_in_file(file_path: &Path) -> Result<i64, String> {
let lines = match std::fs::read_to_string(file_path) {
Ok(s) => s.lines(),
Err(why) => return Err(why.to_string()),
};
let mut sum = 0;
for line in lines {
match line.parse::<i64>() {
Ok(num) => sum += num,
Err(why) => return Err(why.to_string()),
}
}
Ok(sum)
}fn sum_numbers_in_file(file_path: &Path) -> Result<i64, String> {
let lines = match std::fs::read_to_string(file_path) {
Ok(s) => s.lines(),
Err(why) => return Err(why.to_string()),
};
let mut sum = 0;
for line in lines {
match line.parse::<i64>() {
Ok(num) => sum += num,
Err(why) => return Err(why.to_string()),
}
}
Ok(sum)
}? operator? operator to simplify this pattern:fn sum_numbers_in_file(file_path: &Path) -> Result<i64, String> {
let contents = std::fs::read_to_string(file_path)?;
let mut sum = 0;
for line in contents.lines() {
sum += line.parse::<i64>()?;
}
Ok(sum)
}
// Or:
fn sum_numbers_in_file(file_path: &Path) -> Result<i64, String> {
let contents = std::fs::read_to_string(file_path)?;
contents
.lines()
.map(|l| l.parse::<i64>())
.sum() // sum has special handling for Result<T, E>
}? operator (cont.)read_to_string returns Result<String, std::io::Error>parse returns Result<i64, ParseIntError>.map_error(|e| e.to_string())Box<dyn Error>)Error trait:Display lets us convert an error to a human-readable messagesource() lets us nest errors
Result<T, Box<dyn Error>>anyhow crate for this exact type with better ergonomicsanyhowanyhow + contextuse anyhow::{Context, Result};
fn sum_numbers_in_file(file_path: &Path) -> Result<i64> {
let contents = std::fs::read_to_string(file_path)
.with_context(|| format!("failed to read file {}", file_path.display()))?;
let mut sum = 0;
for line in contents.lines() {
sum += line
.parse::<i64>()
.with_context(|| format!("'{line}' is not a valid number"))?;
}
Ok(sum)
}context and with_context to attach information to errors
with_context takes a fn and is executed lazily, good if creating the context is expensive (e.g. allocating strings through format!)thiserror#[derive(Error, Debug)]
enum SumNumbersError {
#[error("file read failed (cause: {0})")]
FileReadFailed(#[from] std::io::Error),
#[error("parsing failed (cause: {0})")]
ParsingFailed(#[from] std::num::ParseIntError),
}
fn sum_numbers_in_file(file_path: &Path) -> Result<i64, SumNumbersError> {
let contents = std::fs::read_to_string(file_path)?;
let mut sum = 0;
for line in contents.lines() {
sum += line.parse::<i64>()?;
}
Ok(sum)
}#[derive(Error)] from the thiserror crate
#[from]anyhow vs. thiserrorSome variant of Option that is None)panic! macropanic!thread 'main' panicked at 'Panic from main', /app/example.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
unwrap() and expect()