ding

create an account to reply

already have one? log in

# [Your Interface Has Two Channels](https://tomeraberba.ch/your-interface-has-two-channels)

This code would easily pass a cursory review:

const response = await fetch('https://example.com/flags.json') const flags = await response.json() startServer(flags)

Then one day the endpoint returns a 500, flags becomes { error: 'Internal Server Error' }, no key matches a real option, and the server silently starts with every default.

fetch doesn’t reject on HTTP errors. It resolves either way, and nothing in the interface tells you to check response.ok. The bug isn’t that you decided to skip error handling. You never realized there was a decision.

Everyone has used an interface that threw them into the Pit of Despair like this. I’ve hit the bottom enough times to notice the pattern.

For each concern an interface exposes, it either forces you to confront it or allows you to inadvertently ignore it. Ignoring a confronted concern is an intentional decision, but ignoring an unknown one commits you to assumptions you didn’t know you made. That signaling determines how the interface fails: by decision or by accident.

Once you see interfaces this way, many familiar design questions become the same. Throw or return an error value? Required parameter or default? Object or union type? Each asks how loudly the interface should signal a concern. Soon you’ll have principles for answering.

## Concern signaling

I’m borrowing the signaling terminology from telecommunications.

In-band signaling means control information travels in the same channel as data. Out-of-band signaling uses a separate channel for control information.

The distinction maps cleanly onto interface concerns. Every interface has the same two channels, and each of its concerns travels on one of them: the channel the user must confront to use the interface at all, or the channel off to the side that they can miss.

## Error handling

Consider a function that returns a union of success and failure. The caller cannot use the function without being aware of the possibility of an error.

For example, returning Rust’s Result<T, E> type forces the caller to explicitly handle the error:

fn parse_config(raw: &str) -> Result<Config, ParseError> { ... } // Trying to use the result without unwrapping would trigger a type error. // If the caller decides to ignore the error, then it's intentional. let result = parse_config(raw); match result { Ok(config) => start_server(config), Err(e) => eprintln!("{e}"), }

In this case the error is in-band. Confronting it is inseparable from using the interface.1

Now consider a function that returns Config and throws on failure. The caller can use the Config directly because the exception requires no acknowledgment.

For example, throwing JavaScript’s Error allows the caller to proceed without confronting the error:

/** @throws Error for invalid configs. */ function parseConfig(raw: string): Config { // ... } // The caller may inadvertently ignore the error if they did not read the // function documentation and are unaware it can throw. const config = parseConfig(raw) startServer(config)

In this case the error is out-of-band. Confronting it requires discipline, and a caller can slip past it without knowing it exists.

A function that throws a checked exception moves the error back in-band. The caller is forced to explicitly catch or propagate the error.

For example, Java’s throws keyword makes error handling in-band: