A blog about software development, written by Daniel Diekmeier. Archive.

Finally Understanding finally in JavaScript

May 7, 2023

I've been writing JavaScript since around 1973, but I've never understood the point of the finally Keyword. The following two implementations of coolFunction are equivalent, so why bother with the additional curly braces?

function coolFunction() {
  try {
    thisFunctionThrows()
  } catch (error) {
    console.log('whoops')
  } finally {
    console.log('dont worry, i got u')
  }
}

function coolFunction() {
  try {
    thisFunctionThrows()
  } catch (error) {
    console.log('whoops')
  }

  console.log('dont worry, i got u')
}

// both of these print the same thing:
coolFunction()
// 'whoops'
// 'dont worry, i got u'

What I didn't understand was that you can use it to run some other code in the last moment before leaving the function – for example if you want to return early or throw an error. This can come in handy when you need to clean up some other resources, no matter in which way you leave the function.

While this can be extremely useful, finally is also not completely straightforward. For example, the order of execution gets a little weird. Consider this example, which surprised me:

function hello() {
  try {
    console.log('hello')
    return 'world'
  } finally {
    console.log('finally')
  }
}

console.log(hello())
// 'hello'
// 'finally'
// 'world'

And while I'm at it, this double-return also feels weird:

function hello() {
  try {
    return "world"
  } finally {
    return "finally"
  }
}

console.log(hello())
// 'finally'

Maybe don't overdo the esoteric stuff.

Time for a Practical Use Case

Here's an example from Eintrittskarten.io (edited for length and width) where we render a PDF from a URL:

async function renderPDF(url, filePath) {
  const browser = await puppeteer.launch({ headless: true })
  const page = await browser.newPage()
  const response = await page.goto(url)
  await page.pdf({ path: filePath })
  await browser.close()
}

Rendering PDFs from URLs is a thankless job. In the past, we've accidentally sent automated emails where we had attached a PDF of our webapp showing an error message. (I actually find this hilarious.) To protect us from these mistakes in the future, we added a check that throws an error if we don't get a response from the server, or if the HTTP status is not 200.

async function renderPDF(url, filePath) {
  const browser = await puppeteer.launch({ headless: true })
  const page = await browser.newPage()
  const response = await page.goto(url)

+ if (!response) {
+   throw new Error(`PDF Renderer: No response from ${url}`)
+ }

+ if (response.status() !== 200) {
+   throw new Error(
+     `PDF Renderer: Expected page status to be 200, ` +
+     `was ${response.status()} for ${url}`
+   )
+ }

  await page.pdf({ path: filePath })
  await browser.close()
}

But sadly, bugs are often fractal, so while this check does save us from sending incorrect PDFs to customers, it introduces a new bug: Whenever we throw an error, the browser we start with puppeteer.launch(...) does not get closed. This can be a problem if your server does not have infinite RAM or is expected to work. Sadly, both of these are true for us.

Combined with automatic retry in the case of errors, I built a magnificent machine that crashes itself whenever there is a problem with the PDFs. Annoyingly, this bug only surfaced while I was at a ramen restaurant, trying to enjoy a nice bowl of Tantan Ramen while rebooting the server from my iPhone.

This is when the usefulness of finally finally dawned on me. This is the perfect use case: I want to make sure that I close the browser, but I also want to be able to throw errors if something goes wrong!

async function renderPDF(url, filePath) {
  const browser = await puppeteer.launch({ headless: true })

+ try {
    const page = await browser.newPage()
    const response = await page.goto(url)

    if (!response) {
      throw new Error(`PDF Renderer: No response from ${url}`)
    }

    if (response.status() !== 200) {
      throw new Error(
        `PDF Renderer: Expected page status to be 200, ` +
        `was ${response.status()} for ${url}`
      )
    }

    await page.pdf({ path: filePath })
+ } finally {
+   // We have to close the browser or it will
+   // keep running in the background.
    await browser.close()
+ }
}

In my local testing, this worked great. (Thankfully, the bug was very reproducible, so I'm pretty confident I fixed it.) This feels pretty good! I'm excited to find out which even tinyer bug hides in the new code, but I'll be ready whenever it shows itself.