Foolproof System

Designing a system that's impossible to misuse is a challenging task. We can't avoid all failures, like a file being locked by another user. Instead, we should focus on eliminating avoidable mistakes, such as writing to a closed file handle or making calls before an object is initialized.

Exposing a feature to users can lead to creative misuses, and even other developers on your team will find ways to misuse it. The key question is: "How difficult am I making it for users to make mistakes?"

The goal should be to make mistakes "very hard" to make, not easy. If a feature is misused, it's often because it was designed to be used in a way that leads to mistakes. By asking ourselves this question, we can design features that minimize mistakes and make it harder for users to make mistakes.

A function invites error

Every C programmer has a favorite function that's easy to misuse - printf. The problem is that printf relies on the format string matching the argument types, leading to unexpected behavior when they don't match.

When you use printf with the wrong argument types, it can lead to unexpected results.

Pulling the format string and its use apart, while keeping hidden dependencies on parameter order, is a recipe for disaster. Translators might accidentally swap parameter order, causing code crashes due to language word ordering differences.

While printf's limitations are understandable, requiring separate arguments is still a bad idea. For example, a function might expect arrays of the same size.

function showAuthorRoyalties(titles: string[], royalties: number[]): void {
    if (titles.length !== royalties.length) {
        throw new Error("Titles and royalties arrays must have the same length.");
    }

    for (let index = 0; index < titles.length; ++index) {
        console.log(`${titles[index]},${royalties[index]}`);
    }
}

When you find a problem, the solutions aren't great. Returning an error when arguments don't match means the caller needs to add error-handling code, which is a sign of a bigger issue.

Alternatively, you could use an throw to check the arguments match. This can result in an immediate crash or a message that's ignored at your own risk - neither option is pleasant.

Leveraging the compiler

It would be better to design the interface to make incorrect usage impossible—or at least to make the compiler reject it. You could combine parallel arrays to eliminate the possibility of mismatched lengths:

You could also collapse related arguments into a single argument:

Solving the localized printf nightmare requires a more complex approach. To ensure type safety and consistent results when arguments are reordered during translation, using strings for all arguments isn't enough.

Instead, create helper functions that format a single argument, returning a field name and the formatted argument. This solution tackles both issues.

You can now detect format string and argument mismatches at runtime. The format string can have custom order for the target language, and printMessage will handle it. If the format string mentions an argument that's not provided, or if it misses one that's provided, it will be logged at runtime. Ideally, this mismatch would be caught by the localization tool before the code runs.

Timing Is Everything

To create foolproof interfaces, detect usage mistakes as early as possible. If not detected, the feature produces incorrect results, leaving the caller to figure it out. This rarely happens.

If mistakes are detected during runtime, it's better than ignoring them. The ideal scenario is reporting the mistake clearly.

The best option is for the compiler to detect the mistake, making it hard to miss. Even better, the system design should make the mistake impossible to express.

No foolproof systems

We can remove most errors by working with compilers or designing systems that can't be misused. Anything the compiler catches makes our job easier.

As Douglas Adams said, people designing foolproof systems underestimate the creativity of fools.

He's right. We can't prevent all mistakes, but we can make it easy to get it right and hard to get it wrong.

Even if we can't make our designs completely foolproof, we should eliminate as many potential errors as possible from the start.