I used to work at a healthcare technology company that was headed on the tech side by a database nut. The data model was both sprawling and very normalized (by that, I mean gnarly). We worked mainly in the exciting realm of medical billing and patient demographic data – particularly involving data with errors and omissions needing correction. Concordantly (😏), a large proportion of the relationships between tables were represented with nullable foreign keys.
I often found myself working on code similar to this contrived simplification …
Figure 1. An example of deeply-nested logic
const getMostRecentFacilityAddressForPatient = async (patientID) => {
const patient = await dataRepository.retrievePatient(patientID);
if (patient !== null) {
if (patient.visitIDs.length !== 0) {
const visitID = patient.visitIDs[patient.visitIDs.length - 1];
const visit = await dataRepository.retrieveVisit(visitID);
if (visit !== null) {
if (visit.facilityID !== null) {
const facility = await dataRepository.retrieveFacility(visit.facilityID);
if (facility !== null && facility.addressID !== null) {
const address = await dataRepository.retrieveAddress(facility.addressID);
return address;
}
}
}
}
}
return null;
};… where the “happy path” was ultimately through many nested layers of negated boolean tests. A textbook case of defensive programming. Besides all of the horizontal scrolling required in our codebase, something about this felt odd to me. Why was the happy path return statement buried in the middle of the function? Sometimes there were else paths, which led to confusion. 😨 Could there be a better way?
Behold—one day I read about guard clauses. I had seen and even used such statements before, but the article or post I read helped crystalize and legitimize the approach. They seemed a potential alternative for structuring functions like Figure 1, so I tried refactoring one of the functions of this sort to use guard clauses …
Figure 2. Logically-equivalent code with guard clauses
const getMostRecentFacilityAddressForPatient = async (patientID) => {
const patient = await dataRepository.retrievePatient(patientID);
if (patient === null) return null;
if (patient.visitIDs.length === 0) return null;
const visitID = patient.visitIDs[patient.visitIDs.length - 1];
const visit = await dataRepository.retrieveVisit(visitID);
if (visit === null) return null;
if (visit.facilityID === null) return null;
const facility = await dataRepository.retrieveFacility(visit.facilityID);
if (facility === null || facility.addressID === null) return null;
const address = await dataRepository.retrieveAddress(facility.addressID);
return address;
};Perhaps because I’m not trained in computer science, I was amazed to see that the desired execution path was now serial statements in the main body of the function (excluding the guard predicates, nested one level deep). This made more sense to me than things being nested an arbitrary n–levels deep. I find the second function both visually and cognitively easier to understand, and, easier to work with and maintain because it avoids bracket-balancing issues and simplifies variable scoping.
Converse to defensive programming, you might call this offensive programming. Writing the error conditions in this way allows nested decision trees for error avoidance to be flattened into a more linear form. A pitfall of structuring logic in nested branches can be made apparent when there’s a need to add additional branches to an existing logic tree – choosing where to place them, or avoiding duplication, can be challenging. Calculating the logical ramifications of adding or moving branches around later is often so painful that one avoids doing that, even when the best solution calls for it. Not everything can be neatly collapsed into serial logic, but I think it’s easier to reason about and maintain than nested logic, and is thus valuable.
One could argue that there is a lot of repetition of “return null” in Figure 2. I agree, but think that a default return value constant would clean that up acceptably. Even more appropriate, potentially, might be to define the contract of the function as “return an address or throw an Error” instead of returning a special value indicating that the function was not able to succeed (null). This simplifies the function’s return type, but leads into a bit of a holy war – some [citation needed] say that exceptions are only to be used when unexpected and unrecoverable (truly “exceptional”) errors occur. I disagree …
The logical inversion I observed by using guard clauses started to inform how I defined functions in the first place. Eventually, I started feeling that functions written with a “fail-fast” approach had a sensible and useful form. If a function could not fulfill its contract, it seemed acceptable—no, expected—that it respond with an error explaining why not. I believe this tends to result in greater modularity, and thus reusability, compared to functions with built-in fault tolerance and error handling.
Figure 3. Guard clauses with exceptions
const getMostRecentFacilityAddressForPatient = async (patientID) => {
const patient = await dataRepository.retrievePatient(patientID);
if (patient === null) throw new NonexistentRecordError({ identifier: patientID, type: 'patient' });
if (patient.visitIDs.length === 0) throw new NonexistentRelationshipError({ childType: 'visit', parentIdentifier: patientID, parentType: 'patient' });
const visitID = patient.visitIDs[patient.visitIDs.length - 1];
const visit = await dataRepository.retrieveVisit(visitID);
if (visit === null) throw new NonexistentRecordError({ identifier: visitID, type: 'visit' });
if (visit.facilityID === null) throw new NonexistentRelationshipError({ childType: 'facility', parentIdentifier: visitID, parentType: 'visit' });
const facility = await dataRepository.retrieveFacility(visit.facilityID);
if (facility === null) throw new NonexistentRecordError({ identifier: visit.facilityID, type: 'facility' });
if (facility.addressID === null) throw new NonexistentRelationshipError({ childType: 'address', parentIdentifier: visit.facilityID, parentType: 'facility' });
const address = await dataRepository.retrieveAddress(facility.addressID);
return address;
};My adoption of guard clauses and the “fail-fast” approach led to an increase in throwing exceptions in the code I wrote. I believe this was simultaneously coincident with a decrease in the number of defects experienced by the end users of my code, however. I stopped fearing exceptions, and wrote code as tho errors would happen, which, well—turned out to be true. Thinking about errors as conditions that occurred in and were handled by the code, rather than them being something that I tried to make the code flow perfectly around, felt liberating and seemed … realistic. It became easier to spot defects before they got to production, since errors were less likely to be obscured by bubbling thru disparate layers of fault tolerance, or potentially swallowed before reaching the surface. Instead, well-placed error-handling routines reported the things my guards were looking for, and many other errors that I didn’t expect.
Speaking of holy wars, if you’ve gone thru the Python tutorial, you might have seen two related paradigms referred to as “look before you leap” and “easier to ask forgiveness than permission”.
This approach isn’t always the most appropriate. At first, it might feel uncomfortable, or maybe even counter-intuitive. It might not fit stylistically or otherwise in your codebase. You might just prefer not to write functions this way (art is subjective). Regardless—I hope the thought experiment was valuable!