Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Graou

Quality gate Coverage Duplicated Lines (%)

Reliability Rating Security Rating

Code Smells Bugs Vulnerabilities

SonarQube Cloud

🐺 What is Graou?

Graou is a lightweight TypeScript library that aims to provide a clean, structured, and reusable approach to creating and managing application errors instead of handling exceptions in an inconsistent way throughout the codebase.

Note

Check our examples to see our way to use it 😀 examples

How to contributes

About the wiki

  1. File naming: The files must be prefix by a two digits number then the page title with a snake_case format.
  2. Create pages: Create a file matching first rule and add it to the SUMMARY.md. The file is ignored if not linked in the summary.
  3. Create a section: Create a directory matching first rule. Create a 00_readme.md or 00_intro.md and add it to the summary. Reason: The summary wont allow non link entry.
  4. Media: If your page contains pictures you must put it inside a folder with the same filename than the markdown file. The folder name didn’t contains the .md extension. Example: src/01_chapter/01_page.md has a media inside src/01_chapter/01_page/my_media.png. The markdown refer it with ![My Media](./01_page/my_media.png).

Core Concept

Create errors

One of the first features of Graou is the error factory, which removes a lot of hassle from error management. In a usual project, the good practice would be to create an error for each use case. If you’re like me, you tend to create a single error and use it everywhere because it’s boring to maintain. Graou removes this issue by providing a factory for errors.

Note

Check out the basic example: TS/JS

Step by step

The first step is to instantiate a factory for your package. The key information is that errors are basically module-scoped. An error factory instance mustn’t be used or exported outside your library/app. You will be able to customize some options for the errors.

import graou from "@carthage-js/graou";

const errorsFactory = graou.makeModuleErrorsFactory({
  moduleName: "<YOUR PROJECT NAME>",
});

You can also provide a factory for the message held by the native error on makeModuleErrorsFactory with messageFactory. The library provides a default pattern, but you can customize it if you need to.

const errorsFactory = graou.makeModuleErrorsFactory({
  moduleName: "<YOUR PROJECT NAME>",
  messageFactory: (
    nodeModule: string,
    scope: string,
    code: string,
    subcode: string | null,
    reason: string,
  ) => "My custom message",
});

With this factory, you will be able to create errors for any case you need. In the library design, you should create a bunch of errors by class or standalone functions. To demonstrate this design, we are going to use this code:

class MyToDoClient {
  private _baseUrl: string;

  constructor(baseUrl: string) {
    this._baseUrl = baseUrl;
  }

  async getVersion(): Promise<string> {
    const response = await fetch(`${this._baseUrl}/version`);
    const body = await response.json();
    return body.version;
  }

  async getToDoList(id: number): Promise<string> {
    const response = await fetch(`${this._baseUrl}/todo_list/${id}`);
    return await response.json();
  }
}

It’s a simple REST API client, but this example doesn’t handle errors. So a 5xx or 4xx status code is not handled. By design, we want to handle this kind of thing, so we add this code to the method:

const response = await fetch(`${this._baseUrl}/...`);
if (response.status < 200 || response.status >= 300) {
  throw new Error("Something went wrong", { cause: response });
}
const body = await response.json();
return body.version;

It’s actually nice because now we know when a request fails. It’s not really good practice to use the basic error directly. The developer won’t be able to distinguish errors in the catch if they are using the same type. So let’s go ahead and add a custom error class:

class MyToDoClientError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
  }
}

We can now replace new Error with new MyToDoClientError. So now, we can distinguish errors issued by the client from the rest of the code. It’s actually what libraries like Axios (HTTP client) do. There is still an issue here. We can’t distinguish which method was called here. If we repeat what we did earlier, it’s pretty easy to handle.

class MyToDoClientError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
  }
}

class MyToDoClientGetVersionError extends MyToDoClientError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
  }
}

class MyToDoClientGetToDoListError extends MyToDoClientError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
  }
}

Then we will be able to throw the error where we need it. It’s starting to feel a bit redundant. So we end up keeping a single error class because it’s easier and quicker to handle. Graou removes this hassle with the factory we created earlier.

const MyToDoClientErrors = errorsFactory("MyToDoClient", ["GetVersion", "GetToDoList"]);

Now we can replace our MyToDoClientError by MyToDoClientErrors.codes.<GetVersion|GetToDoList>.factory. Our file will look like this:

const MyToDoClientErrors = errorsFactory("MyToDoClient", ["GetVersion", "GetToDoList"]);

class MyToDoClient {
  private _baseUrl: string;

  constructor(baseUrl: string) {
    this._baseUrl = baseUrl;
  }

  async getVersion(): Promise<string> {
    const response = await fetch(`${this._baseUrl}/version`);
    if (response.status < 200 || response.status >= 300) {
      throw new MyToDoClientErrors.codes.GetVersion.factory("Something went wrong", {
        cause: response,
      });
    }
    const body = await response.json();
    return body.version;
  }

  async getToDoList(id: number): Promise<string> {
    const response = await fetch(`${this._baseUrl}/todo_list/${id}`);
    if (response.status < 200 || response.status >= 300) {
      throw new MyToDoClientErrors.codes.GetToDoList.factory("Something went wrong", {
        cause: response,
      });
    }
    return await response.json();
  }
}

On usage, we can do things like this:

const client = new MyToDoClient("https://my.todo.mock");
try {
  // ... things with my client
} catch (err) {
  if (err instanceof MyToDoClientErrors.codes.GetVersion.$class) {
    // It's GetVersion that throw an error
  } else if (err instanceof MyToDoClientErrors.scope.$class) {
    // It's a thing related to my client.
  }
}

Note


The reason parameter of the error factory become optional.

Logging

Another good thing that comes with Graou errors. They have a toJson method that helps either send data back to the client or print it in logs. This is particularly useful when working with Datadog, Grafana, or other logging systems.

import graou from "@carthage-js/graou";

try {
  // ... Critical code
} catch (err) {
  if (err instanceof graou.GraouError) {
    console.error(err.toJson());
  }
}

So the library will print this kind of JSON:

{
  "nodeModule": "my_node_module",
  "scope": "MyToDoClient",
  "code": "GetVersion",
  "reason": "Something went wrong",
  "cause": "Raw error message"
}

The resulting JSON sums up the data given to the factory with the data given to the factory method. The cause attribute is either the same structured JSON if it’s a GraouError, or only the message if it’s a basic Error. The cause is not disclosed if it’s something else, to avoid printing sensitive data. You can also control how deep you want to stringify this error with the depth parameter of the method. This way, it won’t show the cause because it can’t go any deeper.

Subcodes

In some cases, you need to provide more details about a method because a single error may not be sufficient. You can create subcodes for a code. A subcode works like a regular code, but it directly inherits from the code class rather than from the scope class.

const errors = errorsFactory("MyScope", ["SimpleCode", "CodeWithSubcode"], {
  CodeWithSubcode: ["CodeA", "CodeB"],
});

// usage
throw errors.codes.CodeWithSubcode.subcodes.CodeA.factory("Hello World !");

Note


The code is no longer abstract when you define subcodes. The whole thing has been rework to play nicely with the error handling and auto errors features.

Class relationship diagram

---
title: Our errors created by the factory
---
classDiagram
    class Error {
        +string name
        +string message
        +string stack
        +any cause

        Error(message: string, options?: ErrorOptions)
    }

    namespace graou {
        class GraouError {
            +nodeModule: string
            +scope: string
            +code: string
            +subcode: string|null
            +reason: string

            +GraouError(nodeModule: string, scope: string, code: string, subcode: string|null, reason: string, fullMessage: string, options?: ErrorOptions)
        }
    }

    Error <|-- GraouError

    namespace yourProject {
        class ScopeError {
            +ScopeError(code: string, subcode: string|null, reason: string, options?: ErrorOptions)
        }

        class CodeAError {
            +CodeAError(reason: string, options?: ErrorOptions)
        }

        class CodeBError {
            +CodeBError(subcode: string|null, reason: string, options?: ErrorOptions)
        }

        class CodeBSubcodeAError {
            +CodeBSubcodeAError(reason: string, options?: ErrorOptions)
        }

        class CodeBSubcodeBError {
            +CodeBSubcodeBError(reason: string, options?: ErrorOptions)
        }

        class CodeCError {
            +CodeCError(reason: string, options?: ErrorOptions)
        }
    }

    GraouError <|-- ScopeError

    ScopeError <|-- CodeAError
    ScopeError <|-- CodeBError
    ScopeError <|-- CodeCError

    CodeBError <|-- CodeBSubcodeAError
    CodeBError <|-- CodeBSubcodeBError

Handle errors

An error created using a factory comes with a set of helper methods to handle different cases throughout the codebase. These methods are not available on a code error when the code has subcodes. However, they are available on errors created from those subcodes.

Note

Check out those examples:

with

This method creates a copy of the code helper with options that affect the behavior of its methods.

reason

You can define a default reason if the factory is called with a null reason.

const errors = errorsFactory("MyScope", ["Code"]);

throw errors.codes.Code.with({ reason: "My reason" }).factory(); // Error will use 'My reason' as error reason.
throw errors.codes.Code.with({ reason: "My reason" }).factory("A reason"); // Error will use 'A reason' as error reason.

uid

Note


Field has been rename from symbol to uid. It’s also no longer a symbol to make it work with lookup feature.

You can define a uid to flag an error and avoid decorate something that you think the base error is suffcient.

const errors = errorsFactory("MyScope", ["CodeA", "CodeB"]);

const uid = "my use case";

throw errors.codes.CodeA.with({ uid }).factory(null, {
  cause: errors.codes.CodeB.with({ uid }).factory(),
}); // Will result into only a CodeB error

We got two major use case that can beneficiate of this feature:

  • Promise: without the symbol all the catch will decorate the previous throwing catch.
const errors = errorsFactory("MyScope", ["CodeA", "CodeB", "CodeC"]);

const uid = "my use case";

const promise = (async () => {
  // ... Critical code
})();

promise
  .catch(errors.codes.CodeA.with({ uid }).$throw)
  .then(() => {
    // ... Critical code
  })
  .catch(errors.codes.CodeB.with({ uid }).$throw)
  .then(() => {
    // ... Critical code
  })
  .catch(errors.codes.CodeC.with({ uid }).$throw);
// Throwing will result into either CodeA, CodeB, CodeC where cause is not a CodeA, CodeB, CodeC error
  • Recusive function: without the symbol each call will decorate the error
const errors = errorsFactory("MyScope", ["Code"]);

const uid = "my use case";

function recurse(steps: number) {
  errors.codes.Code.with({ uid }).trap(() => {
    if (steps >= 0) {
      recurse(steps - 1);
    } else {
      throw new Error("Oupsie");
    }
  });
}
// Throwing will result into a single Code error rather than a Code with [steps - 1] Code cause.

Special case with subcodes

The whole design about using subcode is about to make thing clear about a peculiar case. This way, you decorate only errors that you didn’t expect or didn’t want to manage their. This whole idea is to hydrate information on an error than remove it. So in this case, the subcode is not decorated with the code error.

decorate / $throw

A method that decorate an error with the code error. $throw automaticly rethrow the error. Here some exemples of how to use them:

const errors = errorsFactory("MyScope", ["Code"]);

try {
  // ... Something critical
} catch (err) {
  throw errors.codes.Code.decorate(err);
}

// $throw is usefull to work with promise
// $throw has template parameter to avoid lost typing of the promise.
Promise.resolve(10).catch(errors.codes.Code.$throw<number>);

trap

This function captures errors thrown by both synchronous and asynchronous functions. It uses both of the previously mentioned methods to decorate the error.

const errors = errorsFactory("MyScope", ["Code"]);

const result = errors.codes.Code.trap(() => {
  // ... Critical things
  return "my result";
});

lookup

This function is able to quickly query nested error no matter how big is your error.

const errors = errorsFactory("MyScope", ["Code", "Code2"]);
const errExample = errors.codes.Code.factory(null, {
  cause: errors.codes.Code2.factory(),
});

// usage
const result = errors.codes.Code2.lookup(errExample);
const result = errors.codes.Code2.lookup(() => {
  // Same result than before but it's pretty neat on test unit.
  throw errExample;
});

You can use the uid property to lookup a specific instance when you got twice the same error type.

const errors = errorsFactory("MyScope", ["Code"]);
const errExample = errors.codes.Code.with({ uid: "Code1" }).factory(null, {
  cause: errors.codes.Code.with({ uid: "Code2" }).factory(),
});

// usage
const result = errors.codes.Code.lookup(errExample); // root error
const result = errors.codes.Code.lookup(errExample, "Code1"); // root error
const result = errors.codes.Code2.lookup(errExample, "Code2"); // nested error
const result = errors.codes.Code2.lookup(errExample, "Code3"); // undefined because no error exist with this id

Auto errors

Auto class errors

Note

Check out the example: TS

Graou can edit your class to automaticly implements the whole concepts without the need to do much things.

Important

Only the owned methods of the class are decorated. Inherit methods are not altered.

import graou from "@carthage-js/graou";

const errorsFactory = graou.makeModuleErrorsFactory({
  moduleName: "<YOUR PROJECT NAME>",
});

class MyClass {
  myMethod() {
    // ... my code
  }

  async myAsyncMethod() {
    // ... my code
  }
}

const MyClassDecorated = graou.utils.decorateClassWithErrors(errorsFactory, MyClass);

// Usage
const instance = new MyClassDecorated();
instance.myMethod();

In this example, the resulting class is attach to Errors where the scope is MyClass with 3 codes: constructor, myMethod, myAsyncMethod. Each code matching a method of the class. You got a constructor code no matter what because they are implicitly declarated by the js class. Both methods and constructor on the decorated use the trap mecanism to handle errors. The decorated class define a symbol for each method to handle recursive method.

Graou offer methods to easily access errors that got attached to the class and methods:

const errors = graou.utils.getClassErrors(MyClassDecorated);
const error = graou.utils.getMethodError(MyClassDecorated.prototype.myMethod);

If you allow yourself to use experimentalDecorators on your typescript project then it’s even easier:

import graou from "@carthage-js/graou";

const errorsFactory = graou.makeModuleErrorsFactory({
  moduleName: "<YOUR PROJECT NAME>",
});

@graou.decorators.AutoErrors(errorsFactory)
class MyClass {
  myMethod() {
    // ... my code
  }

  async myAsyncMethod() {
    // ... my code
  }
}

// Usage
const instance = new MyClass();
instance.myMethod();

Note


You can pass directly an errors that match your class because it can be much easier for the typing over the usage. You must aware that errors object must match your class declaration. Otherwise, Graou throw an error due to the mismatch between the two objects.

Bind class errors

Note

Check out the example: TS

Graou offer also an alternate way to alterate your class. The whole reason is due to typing. Make the errors fully generated mean that you must rely upon getClassErrors and getMethodError function to access the basic concepts of Graou for those class. There are not bad but they can create some dumb error because the things are solved during runtime. To avoid that, you can define separetly the class and errors. This way typescript will be able to ensure the typing. Other tools will also be able to issue early error if something is undefined (like webpack). Graou is gonna check the class and errors are matching when he decorate the class with them to avoid issues.

import graou from "@carthage-js/graou";

const errorsFactory = graou.makeModuleErrorsFactory({
  moduleName: "<YOUR PROJECT NAME>",
});

const errors = errorsFactory("MyClass", ["myMethod", "myAsyncMethod"]);

class MyClass {
  myMethod() {
    // ... my code
  }

  async myAsyncMethod() {
    // ... my code
  }
}

const MyClassDecorated = graou.utils.bindClassWithErrors(errors, MyClass);

// Usage
const instance = new MyClassDecorated();
instance.myMethod();