Simple and maintainable error-handling in TypeScript

Sometimes things fail — that’s a fact of life and programming. So as a programmer, you’re going to have to write error-handling code. Thankfully TypeScript has some handy features which can help us to create simple and maintainable error-handling code….


This content originally appeared on DEV Community and was authored by James Elderfield

Sometimes things fail — that's a fact of life and programming. So as a programmer, you're going to have to write error-handling code. Thankfully TypeScript has some handy features which can help us to create simple and maintainable error-handling code.

At Supermetrics one error-handling approach we take is to encode error states into the TypeScript type system. What does this mean? Simply, I’m referring to code where the semantic property of "being an error" is indicated by a variable's type. For a simplified example:

// No information in the type that this is an error,
// you would have to inspect the value to check
let firstError: string = "Something terrible occurred";

interface TerribleError {
  code: "TERRIBLE_ERROR";
  message: string;
}

// It is clearly indicated in the type that this is an error,
// the exact value of the variable is less important
let secondError: TerribleError = {
  code: "TERRIBLE_ERROR",
  message: "Something terrible occurred",
};

So, why is using the type system in this way so great?

1. Potential errors are indicated in function signatures

function doSomethingRisky(): TerribleError | number {
  // ...
}

As a consumer of this function, it’s clear that it may produce an error instead of the expected number. Some developers like to add documentation on potential errors to the function. While documentation is great, it isn’t tied closely to the code and it’s easy for docs and code to diverge over time - in this case either indicating errors that can never occur or missing new errors added later.

2. The compiler will not allow you to forget to check errors

Using the example function from point 1:

const riskyNumber = doSomethingRisky();

// Compiler error because you can't add a TerribleError and a number
const badComputedValue = riskyNumber + 2;

if (typeof riskyNumber === "number") {
  // This is ok as we've guarded against the error case
  const computedValue = riskyNumber + 2;
}

This means you can't forget to check the errors, although it doesn't force you to handle them in any particular way. Simple static analysis like this is a great safety net for developers.

3. It can be used to standardize error handling

When you have a generic type like Error<E> where E is some wrapped data about the error, you now have a generic way of handling errors throughout your codebase. You may even want to go a step further and wrap the good path in some kind of Success type — we often use the pattern of a Result type that is defined as something like type Result<T, E> = Success<T> | Error<E>.

This is incredibly useful for writing generic code like this snippet which implements a function to call a potentially failing function with retries and could be used with any function returning your Result type:

function retry<T, E>(
  func: () => Result<T, E>,
  numberOfAttempts: number
): Result<T, E> {
  let value;

  for (const i = 0; i < numberOfAttempts; ++i) {
    value = func();

    // isError is a simple custom type guard implemented elsewhere
    // https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
    if (!isError(value)) {
      return value;
    }
  }

  return value;
}

Similar patterns can also be useful for many other cases like chaining operations that could fail, memoization of flaky functions, or handling errors from plugins or other 3rd party code.

4. Not all errors are the same

You’ll likely have operations that can fail in many exciting ways, which can also be encoded in these types. For example, by a discriminated union:

interface NetworkError {
  code: "NETWORK_ERROR";
  httpCode: number;
}

// Note that error types can have different properties to include only
// the necessary information
interface EndOfUniverseError {
  code: "END_OF_UNIVERSE";
}

function doVeryRiskyThing(): NetworkError | EndOfUniverseError | null {
  // ...
}

const maybeError = doVeryRiskyThing();

// These type guards cause the type of maybeError to be narrowed within
// the different scopes
if (maybeError?.code === "NETWORK_ERROR") {
  console.log(`Network request failed with code ${maybeError.httpCode}`);
} else if (maybeError?.code === "END_OF_UNIVERSE") {
  panic();
}

5. Function polymorphism can be used to indicate when errors might occur

By having functions that are polymorphic in arguments and return types, you can write very general functions that provide rich information on when errors can occur. For a contrived example, let's say you have an in-memory cache as part of your application and a more-full-featured and longer-term cache as part of another service. You might use a simple flag on your cache function to indicate this like:

// Stores a value to local or remote cache with a given key
function cache<T>(key: string, value: T, useRemoteCache: boolean);

Accessing the remote cache introduces many new failure modes, such as network errors. By writing polymorphic function definitions with your error types, you can indicate this:

// If using local cache then nothing interesting returned
function cache<T>(key: string, value: T, useRemoteCache: false): null;

// If using remote cache we may return a NetworkError
function cache<T>(
  key: string,
  value: T,
  useRemoteCache: true
): NetworkError | null;

// Implementation signature
function cache<T>(
  key: string,
  value: T,
  useRemoteCache: boolean
): NetworkError | null {
  // Implementation here
}

Final words

The above patterns are by no means unique to TypeScript. For example, similar types are commonly used in functional-style programming in other languages, such as Result in Rust or Either in Haskell. You may also spot resemblance in some of these patterns to checked exceptions in Java or the mandatory error handling of error in Go.

It's very easy to build your own versions of the above error handling yourself, and in fact, I'd recommend it as a learning exercise if you want to become more familiar with TypeScript. But of course, there are many packages out there to help you. Some examples — in no particular order — include, purify-ts, fp-ts, and neverthrow. You’ll notice that a couple of those examples are functional programming libraries, this is because errors can be well-modelled with monads.


This content originally appeared on DEV Community and was authored by James Elderfield


Print Share Comment Cite Upload Translate Updates
APA

James Elderfield | Sciencx (2021-05-31T11:31:31+00:00) Simple and maintainable error-handling in TypeScript. Retrieved from https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/

MLA
" » Simple and maintainable error-handling in TypeScript." James Elderfield | Sciencx - Monday May 31, 2021, https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/
HARVARD
James Elderfield | Sciencx Monday May 31, 2021 » Simple and maintainable error-handling in TypeScript., viewed ,<https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/>
VANCOUVER
James Elderfield | Sciencx - » Simple and maintainable error-handling in TypeScript. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/
CHICAGO
" » Simple and maintainable error-handling in TypeScript." James Elderfield | Sciencx - Accessed . https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/
IEEE
" » Simple and maintainable error-handling in TypeScript." James Elderfield | Sciencx [Online]. Available: https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/. [Accessed: ]
rf:citation
» Simple and maintainable error-handling in TypeScript | James Elderfield | Sciencx | https://www.scien.cx/2021/05/31/simple-and-maintainable-error-handling-in-typescript/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.