This is how functional try-catch transforms your JavaScript code

How common is this?

JavaScript
function writeTransactionsToFile(transactions) { let writeStatus; try { fs.writeFileSync('transactions.txt', transactions); writeStatus = 'success'; } catch (error) { writeStatus = 'error'; } // do something with writeStatus... }

It’s yet another instance where we want a value that depends on whether or not there’s an exception.

Normally, you’d most likely create a mutable variable outside the scope for error-free access within and after the try-catch.

But it doesn’t always have to be this way. Not with a functional try-catch.

A pure tryCatch() function avoids mutable variables and encourages maintainability and predictability in our codebase. No external states are modified – tryCatch() encapsulates the entire error-handling logic and produces a single output.

Our catch turns into a one-liner with no need for braces.

JavaScript
function writeTransactionsToFile(transactions) { // 👇 we can use const now const writeStatus = tryCatch({ tryFn: () => { fs.writeFileSync('transactions.txt', transactions); return 'success'; }, catchFn: (error) => 'error'; }); // do something with writeStatus... }

The tryCatch() function

So what does this tryCatch() function look like anyway?

From how we used it above you can already guess the definition:

JavaScript
function tryCatch({ tryFn, catchFn }) { try { return tryFn(); } catch (error) { return catchFn(error); } }

To properly tell the story of what the function does, we ensure explicit parameter names using an object argument – even though there are just two properties.

Because programming isn’t just a means to an end — we’re also telling a story of the objects and data in the codebase from start to finish.

TypeScript is great for cases like this, let’s see how a generically typed tryCatch() could look like:

TypeScript
type TryCatchProps<T> = { tryFn: () => T; catchFn: (error: any) => T; }; function tryCatch<T>({ tryFn, catchFn }: TryCatchProps<T>): T { try { return tryFn(); } catch (error) { return catchFn(error); } }

And we can take it for a spin, let’s rewrite the functional writeTransactionsToFile() in TypeScript:

JavaScript
function writeTransactionsToFile(transactions: string) { // 👇 returns either 'success' or 'error' const writeStatus = tryCatch<'success' | 'error'>({ tryFn: () => { fs.writeFileSync('transaction.txt', transactions); return 'success'; }, catchFn: (error) => return 'error'; }); // do something with writeStatus... }

We use the 'success' | 'error' union type to clamp down on the strings we can return from try and catch callbacks.

Asynchronous handling

No, we don’t need to worry about this at all – if tryFn or catchFn is async then writeTransactionToFile() automatically returns a Promise.

Here’s another try-catch situation most of us should be familiar with: making a network request and handling errors. Here we’re setting an external variable (outside the try-catch) based on whether the request succeeded or not – in a React app we could easily set state with it.

Obviously in a real-world app the request will be asynchronous to avoid blocking the UI thread:

JavaScript
async function comment(comment: string) { type Status = 'error' | 'success'; let commentStatus; try { const response = await fetch('https://api.mywebsite.com/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ comment }), }); if (!response.ok) { commentStatus = 'error'; } else { commentStatus = 'success'; } } catch (error) { commentStatus = 'error'; } // do something with commentStatus... }

Once again we have to create a mutable variable here so it can go into the try-catch and come out victoriously with no scoping errors.

We refactor like before and this time, we async the try and catch functions thereby awaiting the tryCatch():

JavaScript
async function comment(comment: string) { type Status = 'error' | 'success'; // 👇 await because this returns Promise<Status> const commentStatus = await tryCatch<Status>({ tryFn: async () => { const response = await fetch<('https://api.mywebsite.com/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ comment }), }); // 👇 functional conditional return response.ok ? 'success' : 'error'; }, catchFn: async (error) => 'error'; }); // do something with commentStatus... }

Readability, modularity, and single responsibility

Two try-catch rules of thumb to follow when handling exceptions:

  1. The try-catch should be as close to the source of the error as possible, and
  2. Only use one try-catch per function

They will make your code easier to read and maintain in the short- and long-term.

Look at processJSONFile() here, it respects rule 1. The 1st try-catch is solely responsible for handling file-reading errors and nothing else. No more logic will be added to try, so catch will also never change.

And next try-catch in line is just here to deal with JSON parsing.

JavaScript
function processJSONFile(filePath) { let contents; let jsonContents; // First try-catch block to handle file reading errors try { contents = fs.readFileSync(filePath, 'utf8'); } catch (error) { // log errors here contents = null; } // Second try-catch block to handle JSON parsing errors try { jsonContents = JSON.parse(contents); } catch (error) { // log errors here jsonContents = null; } return jsonContents; }

But processJsonFile() completely disregards rule 2, with both try-catch blocks in the same function.

So let’s fix this by refactoring them to their separate functions:

JavaScript
function processJSONFile(filePath) { const contents = getFileContents(filePath); const jsonContents = parseJSON(contents); return jsonContents; } function getFileContents(filePath) { let contents; try { contents = fs.readFileSync(filePath, 'utf8'); } catch (error) { contents = null; } return contents; } function parseJSON(content) { let json; try { json = JSON.parse(content); } catch (error) { json = null; } return json; }

But we have tryCatch() now – we can do better:

JavaScript
function processJSONFile(filePath) { return parseJSON(getFileContents(filePath)); } const getFileContents = (filePath) => tryCatch({ tryFn: () => fs.readFileSync(filePath, 'utf8'), catchFn: () => null, }); const parseJSON = (content) => tryCatch({ tryFn: () => JSON.parse(content), catchFn: () => null, });

We’re doing nothing more than silencing the exceptions – that’s the primary job these new functions have.

If this occurs frequently, why not even create a “silencer” version, returning the try function’s result on success, or nothing on error?

JavaScript
function tryCatch<T>(fn: () => T) { try { return fn(); } catch (error) { return null; } }

Further shortening our code to this:

JavaScript
function processJSONFile(filePath) { return parseJSON(getFileContents(filePath)); } const getFileContents = (filePath) => tryCatch(() => fs.readFileSync(filePath, 'utf8')); const parseJSON = (content) => tryCatch(() => JSON.parse(content));

Side note: When naming identifiers, I say we try as much as possible to use nouns for variables, adjectives for functions, and… adverbs for higher-order functions! Like a story, the code will read more naturally and could be better understood.

So instead of tryCatch, we could use silently:

JavaScript
const getFileContents = (filePath) => silently(() => fs.readFileSync(filePath, 'utf8')); const parseJSON = (content) => silently(() => JSON.parse(content));

If you’ve used @mui/styles or recompose, you’ll see how a ton of their higher-order functions are named with adverbial phrases — withStyles, withState, withProps, etc., and I doubt this was by chance.

Final thoughts

Of course try-catch works perfectly fine on its own.

We aren’t discarding it, but transforming it into a more maintainable and predictable tool. tryCatch() is even just one of the many declarative-friendly functions that use imperative constructs like try-catch under the hood.

If you prefer to stick with direct try-catch, do remember to use the 2 try-catch rules of thumb, to polish your code with valuable modularity and readability enhancements.



11 Amazing New JavaScript Features in ES13

This guide will bring you up to speed with all the latest features added in ECMAScript 13. These powerful new features will modernize your JavaScript with shorter and more expressive code.

11 Amazing New JavaScript Features in ES13

Sign up and receive a free copy immediately.

Leave a Comment

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