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.