This is how functional try-catch transforms your JavaScript code

How common is this?

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.

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:

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:

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:

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:

async function comment(comment: string) { type Status = 'error' | 'success'; let commentStatus; try { const response = await fetch('', { 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():

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<('', { 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.

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:

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:

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?

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

Further shortening our code to this:

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:

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.

Every Crazy Thing JavaScript Does

A captivating guide to the subtle caveats and lesser-known parts of JavaScript.

Every Crazy Thing JavaScript Does

Sign up and receive a free copy immediately.

Leave a Comment

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