try/catch/finally

tldr; Try this? Caught something? Finally, do this!


  try {
    // try doing some tasks
  }
  catch {
    // caught something/errors - let's do something about them
  }
  finally {
    // do something regardless of try/catch
  }

try/catch/finally is a Javascript construct to handle errors. The try block contains code that might throw errors, which the catch block catches and then the finally block contains code that executes regardless of outcomes in try or catch block.

It is helpful when making API calls, hiding progres bars/spinners, logging operations, resetting UI states, resource cleanup and other things.

Sample usage:


  const getData = async <T>(url: string): Promise<T> => {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP error ${response.status}`);
      const data: T = await response.json();
      console.log('API request succeeded');
      return data;
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      console.log(`API request failed: ${message}`);
      throw error;
    } finally {
      console.log('API request completed');
    }
  };

Note: finally always executes (regardless of try/catch block or return/throw/break statements.

But why not keep the finally block code outside?


    try {
      // code that can throw error
    } catch (error) {
      // catch error
    } finally {
      // Why here?
    }
    // And not here?

  

Answer is the finally block is tied to try/catch to ensure it runs after try/catch. Code after try/catch runs only if uncaught errors propogate. In case of finally, it runs even if an error is thrown or caught.

The following code snippet shows this.


  // Correct placement of finally
  try {
    throw new Error('Some error in try');
  } catch (error) {
    console.log('Error caught in catch: ' + error.message);
  } finally {
    console.log('inside finally');
  }

  // Code here runs only if no uncaught errors
  console.log('After try/catch/finally');

  Output:
  Error caught in catch: Some error 
  inside finally
  After try/catch/finally

  

So far, so good, this seems helful. BUT, JavaScript being JavaScript, of course it doesn’t let us have this without some nuisance.

return in finally overrides try/catch


const riskyFinally = () => {
  try {
    console.log('inside try block');
    throw new Error('something failed');
  } catch (error) {
    console.log('inside catch block, caught:', error.message);
    return 'return from catch';
  } finally {
    console.log('inside finally block');
    return 'return from finally'; // Overrides catch return
  }
};

console.log(riskyFinally());

Output:
inside try block
inside catch block, caught: something failed
inside finally block
return from finally
  

return in try executes finally first


const testReturn = () => {
  try {
    console.log('inside try block');
    return 'return from try'; // Return, but finally runs first
  } catch (error) {
    console.log('inside catch block, caught:', error.message);
    return 'return from catch';
  } finally {
    console.log('inside finally block');
  }
};

console.log(testReturn()); 

Output:
inside try block
inside finally block
return from try
  

throw in finally overrides try/catch errors


  const riskyThrow = () => {
    try {
      console.log('inside try block');
      throw new Error('Some error in try');
    } catch (error) {
      console.log('Error caught in catch: ' + error.message);
      throw error; // Re-throw original error
    } finally {
      console.log('inside finally');
      throw new Error('Some error in finally'); // Overrides original error
    }
  };

  try {
    riskyThrow();
  } catch (error) {
    console.log('Outer catch:', error.message); // Logs: Finally error
  }

  Output:
  inside try block
  Error caught in catch: Some error in try
  inside finally
  Outer catch: Some error in finally
  

finally runs even for uncaught errors


  const uncaught = () => {
    try {
      console.log('inside try block');
      throw new Error('Some error in try');
    } finally {
      console.log('inside finally');
    }
  };

  try {
    uncaught();
  } catch (error) {
    console.log('Outer catch:', error.message);
  }

  Output:
  inside try block
  inside finally
  Outer catch: Some error in try

  

finally can modify variables (but return is not affected unless finally explicitiy returns)


  let status = 'initial';
  const modifyInFinally = () => {
    try {
      console.log('inside try block');
      status = 'success';
      return 'from try';
    } catch (error) {
      console.log('Error caught in catch: ' + error.message);
      status = 'error';
      return 'from catch';
    } finally {
      console.log('inside finally block');
      status = 'complete'; // Modifies variable, doesn't affect return
    }
  };

  console.log('returned:', modifyInFinally(), ',', 'status:', status);

  Output:
  inside try block
  inside finally block
  returned: from try , status: complete
  

finally supresses error if returned


  const suppressError = () => {
    try {
      console.log('inside try block');
      throw new Error('Uncaught error');
    } finally {
      console.log('inside finally block');
      return 'Suppressed'; // Error is swallowed
    }
  };
  console.log(suppressError());

  Output:
  inside try block
  inside finally block
  Suppressed

Final thoughts:

The Javascript engine maintains a stack frame for try, throws error to catch, and guarantees finally execution despite the change in return flow. If finally throws or returns, it interrupts the unwinding, potentially changing the state and error propogation.

Despite these flaws, try/catch/finally is indispensable for error handling and deterministic cleanup. There are alternatives though. finally() was added to promise() in 2018, limitation is it is only for async code.


  fetch(url)
    .then(response => response.json())
    .catch(error => console.error(error))
    .finally(() => console.log('Done'));
    

For now, I will continue using try/finally/catch while being mindful of its flaws.

Posted on May 03, 2023