DEV Community

loading...

On useless try-catches, being overly defensive, I/O boundaries and variable scope

rikurouvila profile image Riku Rouvila ・2 min read

Starting point:

async function getUsers() {
  try {
    return await db.select('SELECT * FROM app_users')
  } catch(err) {
    throw err; /* 1. */
  }
}

async function main() {
  try {
    const users = await getUsers()
    console.log(`Ya! We have ${users.length} users!`) /* 2. */
  } catch(err) {
    console.error('Something went wrong..')
  }
}
Enter fullscreen mode Exit fullscreen mode

End result:

function getUsers() {
  return db.select('SELECT * FROM app_users')
}

async function main() {
  let users
  try {
    users = await getUsers()
  } catch(err) {
    console.error('Something went wrong..')
    return
  }
  console.log(`Ya! We have ${users.length} users!`) /* 2. */
}
Enter fullscreen mode Exit fullscreen mode

Step by step

1. If the catch block only rethrows the error, the whole try-catch structure is useless

- async function getUsers() {
-   try {
-     return await db.select('SELECT * FROM app_users')
-   } catch(err) {
-     throw err; /* 1. */
-   }
+ function getUsers() {
+   return db.select('SELECT * FROM app_users')
}
Enter fullscreen mode Exit fullscreen mode

It might be that you used to have some logic inside the catch block, but you removed it and forgot to clean after yourself. Being overprotective with try-catch statements is analogous to if-statements where something is compared to a boolean.

if(variable === true) {
Enter fullscreen mode Exit fullscreen mode

In other words, redundant. Our goal should be to care as little as possible about thrown exceptions and to push exception handling as far up the call stack (as early in execution) as possible. Ideally, our application wouldn't have any try-catch statements.

The theory behind this might be that exception handling is an indicator of side-effects. Because side-effects should be done only at the I/O boundaries of the program, these things would also wrap together quite nicely. Need to clarify my own thinking about this.

1.1 Never await as part of the return expression

With JS promises:

return await db.select('SELECT * FROM app_users')
Enter fullscreen mode Exit fullscreen mode

is the same as:

return db.select('SELECT * FROM app_users')
Enter fullscreen mode Exit fullscreen mode

so I guess we're mostly talking about a syntactical error. This discussion could be expanded to other similar wrapper values, especially lazy ones and how pulling out the value without reason gives less control to the calling function. Now you can get rid of the async keyword as well.

2. The only things allowed in try {} block are things that can throw

async function main() {
+   let users
    try {
-     const users = getUsers()
-     console.log(`Ya! We have ${users.length} users!`) /* 2. */
+     users = getUsers()
    } catch(err) {
      console.error('Something went wrong..')
+     return
    }
+   console.log(`Ya! We have ${users.length} users!`) 
}
Enter fullscreen mode Exit fullscreen mode

Don't put anything else in there. console.log can't throw so it needs to be outside. The reason for this is that the reader of your code cannot know which line of code can actually cause an exception. And yeah, of course, they can go into the function definition and have a look, but we don't want to force the reader to do that. Quite the opposite actually: our aim is to write such code that the reader could understand it only by looking at the directory structure.

Of course, by doing this we have to declare the variable outside try {}s scope, which is admittedly ugly and I do not like it either. It's a small cosmetic sacrifice we do for better readability.

Discussion (3)

pic
Editor guide
Collapse
alainvanhout profile image
Alain Van Hout

As an alternate conceptual approach, you could say that everything inside the try block is the success path, while everything inside the catch block is the failure path.

Collapse
rikurouvila profile image
Riku Rouvila Author

Conceptually I like that a lot. If we'd have a way of defining thrown exceptions like Java's throws syntax, having the whole happy path inside the try block would feel a lot more comfortable.

Collapse
alainvanhout profile image
Alain Van Hout

Yes indeed. It's in fact from my own Java experience that I've come to use the approach 😄.