On
finding
the Happy Path

Clean up your confusing code, with this best practice.

Owen Kelly
Tuesday, June 25, 2019

At some point you will either write, or come across a piece of code that looks something like this contrived snippet.

In this example, we have a function that saves a post, if the post and group variables meet certain conditions.

typescript
src/save.ts
1
// This is a dummy function that just returns true
2
const save = async (userId: string, post: string) => true;
3
4
// Save a post, and return true for success, and false for failure
5
async function savePost(
6
userId: string,
7
post: string,
8
group: string
9
): Promise<boolean> {
10
if (group !== "") {
11
if (group === "author") {
12
if (post.length > 1000) {
13
return await save(userId, post);
14
} else {
15
return false;
16
}
17
}
18
if (group === "admin") {
19
if (post.length > 100) {
20
return await save(userId, post);
21
} else {
22
return false;
23
}
24
}
25
if (post.length > 2000) {
26
return await save(userId, post);
27
}
28
} else {
29
return false;
30
}
31
}
32

When you first develop a function like this, it’s pretty normal to build it up incrementally. Maybe you wrote something like this, which just handled when an author sends a post with more than 1000 characters. And then, a few weeks later the requirement was changed to allow for admin with more than 100 characters. And then anyone with more than 2000 characters.

When you’re building out a feature, code like this is going to be expected at some point. But, if you keep going nesting conditionals it can quickly become confusing. There’s currently 6 ways this function ends. 3 of which attempt to save.

One approach to clean up a function like this is to identify the [happy path]. This is the “default scenario featuring no exceptional or error conditions”.

If you look closely at our first example, you can see there is only one ideal path in this function — it’s the line that call’s save. The happy path is doing nothing.

typescript
src/save.ts
1
if (post.length > 2000) {
2
return await save(userId, post);
3
}
4

Curiously, and this happens a lot in practice, this ideal path is called 3 times in our example. This is the kind of duplication that allows for human error bugs to slip in. If the save functions arguments change, you need to make sure it’s updated in each of these places where it’s called. With a typed language this is less problematic, but it’s still possible to have bugs there.

Because of the nested if statements, it’s not obvious where the happy path is, and the exact conditions to make it happen. This is becomes painful when you need to debug why your function isn’t saving posts it should, or worse, it is saving posts it shouldn’t.

To simplify this function, we want to make the happy path obvious.

Our goal is to ensure the happy path, is not indented and is the last line of the function. This means there’s no condition that must be true to trigger the happy path, and that it is obvious from looking at the function what it’s meant to do.

In our example, we have a few conditionals — which each require slightly different solutions to get as close as possible to our ideal of making the happy path obvious.

The Happy Path

First, let’s look at the easiest change. We check to see if the group variable is not an empty string — otherwise we return false.

typescript
src/save.ts
1
if (group !== "") {
2
// do something
3
} else {
4
return false;
5
}
6

This is the easiest case to fix, as we can just flip the question. By checking for the negative condition, we can return the function early and avoid nesting the rest of our logic. This is commonly called a guard clause.

typescript
src/save.ts
1
if (group === "") {
2
return false;
3
}
4

Next up, we have a a couple of nested if statements, that can be paired together.

typescript
src/save.ts
1
if (group === "author" && post.length > 1000) {
2
return await save(userId, post);
3
} else {
4
return false;
5
}
6
if (group === "admin" && post.length > 100) {
7
return await save(userId, post);
8
} else {
9
return false;
10
}
11

Adding Guardrails

This looks simple enough, but we would still have two calls to save. Ignoring that, the code would also not function correctly. Because the author conditional is evaluated first, if it fails the admin conditional will never run because the function will return before that.

We can’t easily flip these conditions to check the negative. When you want to check for two conditions at the same time, checking for the negative of those can be painful. Maybe not for you while you write it, but definitely for you six months later, debugging during a production incident. Furthermore, in some cases they’re impossible.

For these problems, I like to use a boolean that is initialised to false that can be switched to true when the condition for the happy path is met. This is a practice that reduces the conditions down to a single boolean.

If our example below we set canSave to false at the very start of the function. And just before the end we check if it is still false and return.

typescript
src/save.ts
1
// Save a post, and return true for success, and false for failure
2
async function savePost(
3
userId: string,
4
post: string,
5
group: string
6
): Promise<boolean> {
7
let canSave = false;
8
9
if (group === "") {
10
return false;
11
}
12
13
if (group === "author" && post.length > 1000) {
14
canSave = true;
15
}
16
if (group === "admin" && post.length > 100) {
17
canSave = true;
18
}
19
20
if (post.length > 2000) {
21
canSave = true;
22
}
23
24
if (!canSave) {
25
return false;
26
}
27
28
return await save(userId, post);
29
}
30

I’ve opted to return early if the first condition checking the group is true, to prevent wasting time on the rest of the conditions.

This isn’t the only way to solve this problem, there are many. The two things I wanted to show is how using either a Guard Clause or reducing complex conditions to a boolean can help untangle complicated functions, and make the happy path obvious.

— OK
Tagged