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