[Javascript] Controlling code flow (3)

try, catch, and finally

When we try to handle errors, try-catch statement is a good option.

However, it isn’t clear how the error is caught in catch block. Furthermore, it is trickier when finally block is included. We do know that finally block must be executed after try and catch block, but the code flow is not so clear.

Let’s check how try,catch, and finally block works.

try and catch

We write error-prone code in the try block and throw an error if some condition is met, or the JavaScript Runtime automatically throws an error.

The error is caught by the nearest catch block.

What do we mean by “nearest“?

The nearest catch block in the context of the call stack catches the error. The error propagates up the call stack, and if a catch block is encountered, it catches the error.

Let’s see an example.

function functionA() {
  try {
    functionB();
  } catch (error) {
    console.log("Caught in functionA:", error.message);
  }
}

function functionB() {
  try {
    functionC();
  } catch (error) {
    console.log("Caught in functionB:", error.message);
    throw error;
  }
}

function functionC() {
  throw new Error("An error occurred in functionC");
}

functionA();

// Result:
//Caught in functionB: An error occurred in functionC
//Caught in functionA: An error occurred in functionC

The error thrown in functionC is caught by the catch block in functionB because it is the nearest catch block in the call stack.

functionA()
┌────────────────────────────────────────────────────────────────┐
try {                                                          │
functionB()                                                │
│     ┌───────────────────────────────────────────────────────┐  │
│     │ try {                                                 │  │
│     │     functionC()                                       │  │
│     │     ┌──────────────────────────────────────────────┐  │  │
│     │     │ functionC() {                                │  │  │
│     │     │     throw new Error("An error occurred in    │  │  
│     │     │     functionC")                              │  │  
│     │     │ }                                            │  │  │
│     │     └──────────────────────────────────────────────┘  │  │
│     │ } catch (error) {                                     │  │
│     │     console.log("Caught in functionB:", error.message)│  │
│     │     throw error                                       │  │
│     │ }                                                     │  │
│     └───────────────────────────────────────────────────────┘  │
│ } catch (error) {                                              │
│     console.log("Caught in functionA:", error.message)         │
│ }                                                              │
└────────────────────────────────────────────────────────────────┘

The visualized call stack above makes it much easier to understand.

finally

t gets much trickier with the finally block. The finally block is always executed after the try-catch block.

Before proceeding to a detailed explanation, try to predict the result of the code below.

function example() {
  try {
    console.log("In try block");
    throw new Error("Error in try block");
  } catch (e) {
    console.log("In catch block");
    throw new Error("Error in catch block");
  } finally {
    console.log("In finally block");
    return "Return from finally block";
  }
}

try {
  console.log(example());
} catch (e) {
  console.log("Caught in outer catch: " + e.message);
}

// Answer
In try block
In catch block
In finally block
Return from finally block

Quite tricky right?

The logic is as follows.

  • The error thrown in the try block is caught in the catch block in the same stack frame.
  • The error thrown in the catch block is propagated to the upper stack frame. However, the finally block should be executed before moving on to the upper stack.
  • Therefore, the return statement blocks error propagation to the upper stack since the code flow is already controlled by the return statement. Thus, the stack frame has moved to the upper level before handling the error.

If there wasn’t a return statement in the finally block, the answer would be different.

function example() {
  try {
    console.log("In try block");
    throw new Error("Error in try block");
  } catch (e) {
    console.log("In catch block");
    throw new Error("Error in catch block");
  } finally {
    console.log("In finally block");
  }
}

try {
  console.log(example());
} catch (e) {
  console.log("Caught in outer catch: " + e.message);
}

//Answer
In try block
In catch block
In finally block
Caught in outer catch: Error in catch block

Conclusion

The finally block is always executed after the try-catch block. If a return statement is present in the finally block, it overrides any throw statements in the catch block, preventing the error from propagating to the upper stack frame. Without a return statement in the finally block, errors thrown in the catch block will propagate to the upper stack frame and can be caught by an outer catch block after the execution of the finally block.

Leave a Reply

Your email address will not be published. Required fields are marked *