dio.la

Dani Guardiola’s blog

Nov 28, 2023November 28th, 2023 · 1 minute read · tweet

Try, return, finally: a curious JavaScript pattern

There's life beyond the return if you're willing to try.

return is the end

One of the first things you'll learn in JavaScript is that the return keyword ends a function's execution.

For instance, the extremely common "early return" pattern relies on this fact.

ts
function earlyReturn() {
if (someCondition) return "early";
 
// no "else" needed
someOtherLogic();
}
Try

This behavior has some implications. Consider the following example.

ts
function doSomething() {
return getOutputValue();
 
someOtherLogic();
}
Try

If you do this, someOtherLogic will never be called, because the function exits after the return statement. This is called "dead code", and some tools will even automatically remove it for you.

A common way to work around this is to store the output value to be able to return it afterward.

ts
function doSomething() {
const result = getOutputValue();
 
someOtherLogic();
 
return result;
}
Try

One classic example is a React context consumer which ensures the context is present.

ts
function useMyContext() {
const value = useContext(MyContext);
 
if (!value) throw new Error("MyContext is not available");
 
return value;
}
Try

Life beyond return

Now, consider this.

ts
function doSomething() {
try {
return getOutputValue();
} finally {
someOtherLogic();
}
}
Try

Will someOtherLogic be called? Surprisingly, the answer is yes. Specifically, this is what will happen:

  1. The try block is executed.
  2. The getOutputValue() function is called.
  3. The finally block is executed.
  4. The someOtherLogic() function is called.
  5. The output value of the previous getOutputValue() call is returned.

I found out about this after reading the source of Lexical's readEditorState function, where some cleanup logic needs to run before returning the output value.

ts
export function readEditorState<V>(
editorState: EditorState,
callbackFn: () => V
): V {
const previousActiveEditorState = activeEditorState;
const previousReadOnlyMode = isReadOnlyMode;
const previousActiveEditor = activeEditor;
 
activeEditorState = editorState;
isReadOnlyMode = true;
activeEditor = null;
 
try {
return callbackFn();
} finally {
activeEditorState = previousActiveEditorState;
isReadOnlyMode = previousReadOnlyMode;
activeEditor = previousActiveEditor;
}
}
Try

If you're curious, I wrote about this and more in my Lexical state updates article.


Is this a good idea? I don't know. It's clever, definitely, but it can also be a bit surprising.

Bonus: return in finally

Can you guess what will be logged when the following code runs?

ts
function doSomething() {
try {
return "try";
} finally {
return "finally";
}
}
 
console.log(doSomething());
Try

The answer is "finally". The return in the finally block overrides the return in the try block.

Here's a sandbox if you want to see for yourself. Open the console to see the output.

Stay weird, JavaScript.


PD: here are some related places in the V8 and Chromium codebases.

Thank you @spirobel (on Discord) for sharing these with me!

Keep up with my stuff. Zero spam.