Introduction

ajel

What is ajel?

ajel is a set of thin wrappers for try-catch, coupled with eslint rules to encourage better error handling.

Installation

yarn add ajel eslint-plugin-ajel

pnpm add ajel eslint-plugin-ajel

Usage

// Handling async functions that throw
import { ajel } from 'ajel';
 
async function main() {
  const result = await ajel(Promise.resolve('hello world'));
 
	// Accessing result before narrowing union type throws eslint error
  // console.log(result);
 
  if (result instanceof Error) {
    // This return narrows the type of result to its Result
    return;
  }
 
  return result;
}
// Handling syncronous functions that throw
import { sjel } from 'ajel';
 
async function main() {
  const result = sjel(JSON.parse, "{}");
 
	// Accessing result before narrowing union type throws eslint error
  // console.log(result);
 
  if (result instanceof Error) {
    // This return narrows the type of result to its Result
    return;
  }
 
  return result;
}

ajel and sjel are thin wrappers for try-catch, coupled with eslint rules to encourage better error handling.

The linting tools are available in the package eslint-plugin-ajel

Basic eslintrc

{
  plugins: ['ajel'],
  extends: [
    'plugin:ajel/recommended',
  ],
}

Why I created AJEL

As I entered yet another year of employment at my current company, I was tasked with a new goal. I wanted to learn something new. A little mustached Netflix birdie told me to dig one level deeper. I told myself it was time to stop being a lowly Typescript soy latte dev, and alas, I had a brief but impactful affair with the oxidized language Rust. As work demands kicked into overdrive during the busy portion of the year, I returned to Typescript land with my new experience with Result types - and I saw nothing but holes everywhere.

It greatly bothered me that I had never fully considered how to properly handle errors in my code. Thus began my exploration of the topic. It was not hard to narrow down the source of the problem. try/catch is a terrible way to handle errors. More specifically, the problem is the throw keyword is not included in the type system, and thus, it is not possible to know what errors a function can throw.

I tried a few different approaches. Result-type libraries felt like trying to force Rust's paradigm into a language that wasn't designed for it. I wanted to find a way to with the language, not against it.

More Advanced Example

import { ajel, sjel } from 'ajel';
 
class CustomError extends Error { }
class CustomError2 extends Error { }
class CustomError3 extends Error { }
 
const foobarFn = async () => {
  const result = await ajel(Promise.resolve('result'));
  const result2 = sjel(JSON.parse, '{}');
  const result3 = await ajel(Promise.resolve('result'));
  const result5 = sjel((stringvar: string) => stringvar, '{}');
 
  //------ Test2 SJEL
  if (result2 instanceof CustomError) {
    //We can access the error here in BinaryExpression with var instanceof
    console.log(result2);
    return;
  }
 
  // Cant Access the result2 variable here
  // console.log(result2);
 
  if (result2 instanceof Error) {
    console.log(result2);
    // This return narrows the type of result2 to its Result
    return;
  }
  // Type is narrowed - no longer a union of Result | Error -> just Result
  console.log(result2);
 
  //------ Test AJEL
  // Cant Access the result variable here
  // console.log(result);
 
  switch (true) {
    case result instanceof CustomError3:
      //We can access the error here in BinaryExpression with var instanceof
      console.log(test);
      break;
    //We support fall through
    case result instanceof CustomError2:
    case result instanceof CustomError:
      console.log(result);
      break;
    case result instanceof Error:
      break;
  }
  console.log(result);
 
  //---- No handling of AJEL and SJEL returns reports Errors
  // console.log(result3);
  // console.log(result5);
};