Error Handling in JavaScript: Stable Software in an Unknown World (Part 1)

Luke Brandon Farrell - Lead Mobile Developer

Lately, I have been thinking about how to achieve stable software in a JavaScript universe of dependencies of varying quality and little standardisation between them. Which doesn’t involve wrapping your whole application in a try…catch statement, which obviously doesn’t allow you to handle errors with the care they deserve.

This article is going to be about error handling, in particular this part of the series is going to be about handling unknown dependencies “the unknown sea” where many monster may live, some are friendly and well tamed, but some are very scary, and may eat you, your team, and your full application.

Photo by David Menidrey on Unsplash

So let’s talk about dependencies in the JavaScript universe. Dependencies are great and the open-source community is a backbone of the JavaScript ecosystem, and they save developers and applications a lot of time in achieving what they want, but the problem here is we never know what we are getting, a single method from a third party dependancy can bring down your full application.

So how do we prevent this? That is where the concept of Adapters come in. An adapter in the real world allows one system to communicate or transmit information to another, they process information and include safety features, like the adapters you use to convert the voltage of your appliances when you go to Europe or visa-versa 🔌.

Photo by Markus Winkler on Unsplash

Adapters in JavaScript wrap a particular dependancy to add any additional processing and handle any errors gracefully. Let’s begin.

Data Processing with Adapters

Adapters can been used to take X data structure and transform it to Y data structure where Y is a data structure defined to our application codebase.

For example, let’s say we have a picker library to pick attachments from the mobile library, and those attachments get returned in a data structure called `Asset` but we have a custom data structure to work with assets in our application called `MediaObject` we can use an adapter to wrap the picker library and make sure each Asset is processed into a MediaObject as such:

export async function launchImageLibraryAsync() : MediaObject {
 try {
   // ‘launchImageLibrary’ npm dependancy
   const asset = await launchImageLibrary(options);
   // process media object from asset
   const result = MediaObject.fromAsset(asset);    return result;
 } catch(e) {
   // process error
 }
}

Now with this approach we process all data coming into our application from third-party dependencies which has the following benefits; it gives us a layer of security from unknown sources and it allows us to work with custom defined structure built for our application which makes it easier to swap out dependencies for another in the future.

Adapters are built with the purpose of processing data from one form to another.

Error Handing with Adapters — Should Adapters handle view logic?

Adapters by design should not handle any view logic. Meaning, that if there is an error we shouldn’t be notifying a user at that point with an Alert or Modal. Adapters are built with the purpose of processing data from one form to another.This includes errors.

Stop for a second and think… every dependancy could throw an error and crash your application. The `launchImageLibrary` method is unknown and could throw an error at any point. That is why it is essential to wrap every dependancy in a `try…catch` and handle the error, well, not just handle the error but return an error which can be handled by your application.

At Qeepsake we use the neverthrow package to handle errors. The adapters job isn’t to catch the error and immediately throw another, adapters should never blow up! Or crash your application. Instead you should return errors from adapters. For example here is an adapter which returns an object as an error:

try {
 …
 if (result.errorCode === ‘camera_unavailable’) {
   return new ErrorResult({
     code: ‘CAMERA_NOT_SUPPORTED’,
     message: ‘Camera is NOT supported by this device’,
   });
 } else if (result.errorCode === ‘permission’) {
   return new ErrorResult({
    code: ‘CAMERA_NO_PERMISSION’,
    message: ‘Device does NOT have permission to access the camera’,
   });
} else if (result.didCancel) {
  return new ErrorResult({
   code: ‘CAMERA_CANCELLED’,  
   message: ‘User cancelled from the camera’,
  });
} else if (result.errorMessage === ‘Unsupported file type’) {
 return new ErrorResult({
  code: ‘CAMERA_UNSUPPORTED_TYPE’,
  message: ‘MIME type selected from the camera is not supported’,
 });
} else {
 return new ErrorResult({
  code: ‘CAMERA_NO_DATA’,
  message: ‘No data returned from the camera’,
 });
}
} catch (e) {
 // Maybe track an error here
 return new ErrorResult({
  code: ‘CAMERA_UNKNOWN_ERROR’,
  message: ‘An unknown error occrured when using the camera’,
 });
}

The return type of the function than would look something like `MediaObject | ErrorResult`. You would then handle the ErrorResult in the code which uses the adapter to display an error message or proceed with a successful result. Remember… the idea here is to handle unknown output and convert it into something our application understands and can work with.

Adapters are a layer between you and the unknown to built fault tolerance into your applications.

To Adapt or Not to Adapt, that is the question

A question naturally arrises of what dependencies should be adapted and which ones should not, this isn’t an easy question. You are likely to find some grey area here. The best template I have to date is adapt libraries not frameworks, although some libraries you may not want to adapt i.e. lodash. You must use your own intuition to figure this out… as of yet I haven’t created or came across a standard rules set, although I will share some examples of Qeepsake dependencies and what should be adapted and not adapted

  • @alessiocancian/react-native-actionsheet : adapt ✅ (see below we cover adapting components)
  • @react-native-async-storage/async-storage : adapt ✅ you never know when you may swap out your storage implementation!
  • @react-native-community/cameraroll : adapt ✅ it’s an imperative API why wouldn’t we adapt it!?
  • @react-native-community/datetimepicker : adapt ✅ another component
  • @react-native-community/netinfo : adapt ✅
  • @react-native-firebase/analytics : adapt ✅ into an analytics class (easy to swap out in the future)
  • @react-navigation/stack : not adapt✖️ this behaves more like a framework
  • expo-file-system : adapt ✅
  • expo-video-thumbnails : adapt ✅
  • formik : not adapt✖️formic acts like a framework for forms
  • react : not adapt✖️
  • react-native : not adapt✖️
  • react-native-animatable : adapt ✅ it’s good practice as it means swapping out your animation library may be easier plus allows you to rename components correctly to not conflict with react-native `View`, `Image` etc.
  • react-native-view-shot : adapt ✅ you can adapt the reference methods on this one also to handle errors correctly
  • react-redux : not adapt✖️acts as framework
  • react-relay : not adapt✖️acts as framework
  • redux : not adapt✖️acts as framework
  • redux-persist : not adapt✖️framework extension

As you can see there isn’t a clear answer on which dependencies to wrap with adapters.

Folder Structure of Adapters

I have advocated for flat folder structures and that doesn’t change when coming to adapters. Adapters should be their own root level folder, your folder structure may look like this:

/pages
/components
/adapters
/utils

Adapters can be functions, classes or components with the intention of providing a safe communication layer between dependencies and the rest of our application. Everything which falls into that definition belongs in adapters.

Mocking of Third Party Libraries

Adapters also allow us to easily mock third party libraries now that you have a layer between your application business logic and the dependencies, we can easily write mock implementation for those dependencies which sit alongside our adapters, and make it much less likely you will gave to restructure parts of your application to write mocks.

Extending and Combining Dependencies via Adapters

Adapters aren’t just to build one to one communication with dependencies but they can also be used to combine a multitude of dependencies and extend the functionality of those dependencies. For example we have a file in Qeepsake which adapts the `makeDirectoryAsync` method from the Expo File System API, in which we have added extra functionality, to provide better support for returning the directory, the following example will return the directory if it already exists and won’t throw an error:

static async makeDirectoryAsync(
 directory: string,
 options?: { intermediates: boolean },
): Promise<ResultAsync<string,   ErrorResult<’MAKE_DIRECTORY_UNKNOWN_ERROR’>>> {
 try {
   const { exists } = await ExpoFileSystem.getInfoAsync(directory);

   if (!exists) {
     await ExpoFileSystem.makeDirectoryAsync(directory, options);
   }

   return okAsync(directory);
 } catch (e) {
   trackException(e);    return errAsync({
     code: ‘MAKE_DIRECTORY_UNKNOWN_ERROR’,
     message: ‘An unknown error occrured when creating the directory: ‘ + directory,
   });
 }
}

Also, as well as improving functionality of existing dependencies, you could combine multiple dependencies under the same Adapter. Say you had a FileSystem class, you may pull in dependencies from expo-file-system, get MIME type methods, and other methods to access EXIF data all under the same file system class, working with multiple dependencies to give you a robust file system class for your business logic to utilise.

It’s put to you how you structure your adapters. There isn’t a right way. It makes sense to structure them in a way which compliments your application.

Can we write adapters for components?

Yes. You should wrap dependencies which are components as adapters. This allows you to adapt reference method and build error boundaries around the component. The goal of adapters is to make sure we catch and handle error by passing them back in a way that doesn’t crash our application. So when wrapping your dependencies with error boundaries in adapters, you should ask, what is the fallback experience if this component crashes?

If you want to get really advanced with your adapters you can even build in fallback UI and functionality incase one method borks!

Finally, zooming out and thinking about Business Logic…

There is another idea which isn’t fully developed but as I may be talking about it in the future I am going to briefly discuss it here and that is the idea of business logic as it’s own layer, for example you may have adapted a HTTP library such as Axios to return formatted errors and add some extra functionality, although there may be another layer where our business logic comes into play, we may have a number of functions: fetchOrders, fetchCustomers, fetchTags which wrap that Axios adapter but still return errors and don’t handle any view logic (i.e. displaying and error to the user). We would then have a function like loadOrderswhich pulls in the fetchOrders function and will handle any errors and display feedback to the user. loadOrders would be called from a component. A layer type approach like so:

Diagram of Logic (Adapters -> Business Logic -> View Logic)

Don’t take this as advice on how to structure your application, this final part was some closing thoughts on how we can build a organised architecture for business logic, allowing us to build methods without side effects (or know side effects). There is a lot to be developed here and I hope to come back with some more answers shortly… for now… adapt a way!

Thanks for reading. If you enjoyed this then go and checkout Qeepsake.com which uses a lot of these methodologies under the hood!

Recommended Read

Subscribe to our Newsletter

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.