Search code examples
javascripttry-finally

Doing clean-up inside finally block, with return in try block - bad practice?


I have this code to figure out the width of some text:

function textWidth(text, font) {
    const span = document.createElement('span')
    try {
        span.style.cssText = `position: absolute; opacity: 0; font: ${font || "'Arial'"};`
        span.innerText = text
        document.body.appendChild(span)

        return span.offsetWidth
    } finally {
        document.body.removeChild(span)
    }
}

Is it bad practice to do the clean-up inside finally when the try block is returning something? It works as expected; this question is not about how try...finally works, I get that part. I'm just wondering if it would for some reason be better to store the width in a variable, do the clean-up, and then return the variable?


Solution

  • Is it bad practice to do the clean-up inside finally when the try block is returning something?

    No, it's perfectly normal practice.

    When you do return span.offsetWidth, the value of span.offsetWidth is evaluated and put to one side, then the finally code is run, and then that value is returned. No need to do that manually. Your finally could even have span = null; in it, and it wouldn't affect your return value because the expression span.offsetWidth has already been evaluated at that point.

    You can see that in the spec:

    Runtime Semantics: Evaluation

    ReturnStatement: return Expression;

    1. Let exprRef be the result of evaluating Expression.
    2. Let exprValue be ? GetValue(exprRef).
    3. If ! GetGeneratorKind() is async, set exprValue to ? Await(exprValue).
    4. Return Completion { [[Type]]: return, [[Value]]: exprValue, [[Target]]: empty }.

    In that excerpt, the value is in exprValue, which becomes part of the completion record (that's a spec mechanism, not necessarily a real thing); then the finally code is run, and the function completes with that completion record (unless of course, finally throws or issues a different return that supercedes this return).

    And you can also observe it:

    function example() {
      let foo = () => {
        console.log("foo called");
        return 42;
      };
      try {
        console.log("Doing: return foo();");
        return foo();
      } finally {
        console.log("finally block, doing foo = null");
        foo = null;
      }
    }
    console.log(example());