This post is another one by InRhythm’s own Carl Vitullo. For the full post and additional links, check out the original “Overly Defensive Programming” on his medium channel.
I recently asked a coworker why a certain check was being done, and he answered with a shrug and said, “Just to be safe.” Over my career, I’ve seen a lot of code written just to be safe. I’ve written a lot of that code myself! If I wasn’t sure if I could rely on something, I’d add a safety check to prevent it from throwing an exception.
To give some examples, I mean idioms like providing unnecessary default values.
axios.get(url).then(({ data }) => // If the response doesn't have a document, use an empty object this.setState({ document: data.document || {} }); })
Or checking that each key exists in deeply nested data.
render() { const { document } = this.state; const title = document && document.page && document.page.heading && document.page.heading.title;
return <h1>{title}</h1> }
And many other idioms. Idioms like these prevent exceptions from being thrown. Used without care, suppressing an exception is like hanging art over a hole in the wall.
At a glance, there doesn’t appear to be a problem. But you haven’t patched the hole and you haven’t fixed the bug. Instead of an easy-to-trace exception, you have unusable values — bad data — infiltrating your program. What if there’s a bad deployment on the backend and it begins returning an empty response? Your default value gets used, your chain of &&
checks returns undefined
, and the string ‘undefined’ gets put on your page. In React code, it won’t render anything at all.
There’s an adage in computing, “be liberal in what you accept and conservative in what you send.” Some might argue that these are examples of this principle in action, but I say disagree. I think these patterns, when used to excess, show a lack of understanding of what guarantees your libraries and services provide.
Data or arguments from third parties
What your code expects from somebody else’s code is a contract. Often, this contract is only implied, but care should be taken to identify what form the data take and to document it. Without a well understood, clearly documented response format from an API, how can you tell whose code is in error when something breaks? Having a clear definition builds trust.
When you request data from an external HTTP API, you don’t need to inspect the response
object to see if it has data
. You already know that it exists because of the contract you have with your request library. For a specific example, the axios documentation defines a schema for the format the response comes back with. Further, you should know the shape of the data in the response. Unless the request is stateful or encounters an error, you’ll get the same response every time — this is the contract you have with the backend.
Data passed within the application
The functions you write and the classes you create are also contracts, but it’s up to you as a developer to enforce them. Trust in your data, and your code will be more predictable and your failure cases more obvious. Data errors are simpler to debug if an error is thrown close to the source of the bad data.
Unnecessary safety means that functions will continue to silently pass bad data until it gets to a function that isn’t overly safe. This causes errors to manifest in a strange behavior somewhere in the middle of your application, which can be hard to track with automated tools. Debugging it means tracking the error back to find where the bad data was introduced.
I’ve set up a code sandbox with an example of overly safe and unsafe accesses.
const initialStuff = { things: { meta: { title: "I'm so meta, even this acronym", description: "will throw an error if you break the data" } } };
// And within each component, handleClick = e => { if (this.state.stuff) { this.setState({ stuff: null }); } else { this.setState({ stuff: initialStuff }); } };
The “safe” component guards against exceptions being thrown.
const { title, description } = (stuff && stuff.things && stuff.things.meta) || {};
And the unsafe one gets the values without any checks.
const { title, description } = this.state.stuff.things.meta;
This approximates what could happen if an external API starts returning unusable data. Which of these failure modes would you rather diagnose?
[…] we shared an article written by InRhythm’s own Carl Vitullo on Overly Defensive Programming. That article was the basis for a talk he gave at JavaScript.NYC last month and the video is […]