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.
tsTryfunction earlyReturn() {if (someCondition) return "early";// no "else" neededsomeOtherLogic();}
This behavior has some implications. Consider the following example.
tsTryfunction doSomething() {return getOutputValue();someOtherLogic();}
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.
tsTryfunction doSomething() {const result = getOutputValue();someOtherLogic();return result;}
One classic example is a React context consumer which ensures the context is present.
tsTryfunction useMyContext() {const value = useContext(MyContext);if (!value) throw new Error("MyContext is not available");return value;}
Life beyond return
Now, consider this.
tsTryfunction doSomething() {try {return getOutputValue();} finally {someOtherLogic();}}
Will someOtherLogic be called? Surprisingly, the answer is yes. Specifically, this is what will happen:
- The
tryblock is executed. - The
getOutputValue()function is called. - The
finallyblock is executed. - The
someOtherLogic()function is called. - 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.
tsTryexport 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;}}
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?
tsTryfunction doSomething() {try {return "try";} finally {return "finally";}}console.log(doSomething());
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.
- A test for
returninfinally TryCatchclass headersTryCatchclass reference- Chromium wrapper of the
TryCatchclass
Thank you @spirobel (on Discord) for sharing these with me!