If you have a language with enum types (Haskell, ML, etc., or any of the languages inspired by them like Rust, Scala, etc.), that's straightforward. In e.g. Rust, you'd have
enum Connection {
ValidConnection {fd: RawFd, address: SockAddr, ...},
BadAddr,
BadPort,
...
}
In fact Rust's representation of the maybe type is just a generic
enum Option<T> {
Some(T),
None,
}
so all you're doing is getting rid of the two layer Option<Connection> syntax and making it a single Connection, so you're not writing Option<this>, PossiblyBadAddress<that>, etc. everywhere.
Then all your call sites can do one of two things. Either they can check for specific errors, like
match conn {
Connection {fd, address, ..} => write(fd, ...),
BadAddr => /* handle error */,
BadPort => /* handle error */,
}
or you can write a simple function that throws an exception, if you really want an exception-handling style:
impl Connection {
fn get_fd(&self) -> RawFd {
match *self {
ValidConnection {fd, ..} => fd,
BadAddr => panic!("The given address was not correct"),
BadPort => panic!("The given port was not correct"),
}
}
}
and then call conn.get_fd(). For a simple command-line app, panicking and aborting is pretty much what you'd want to do anyway. For a test suite, panics are caught per test case (and you can even test that a function
does panic with a given message), so it's very testable.