defekt
    TypeScript icon, indicating that this package has built-in type declarations

    9.1.1 • Public • Published

    defekt

    defekt is custom errors made simple.

    defekt

    Status

    Category Status
    Version npm
    Dependencies David
    Dev dependencies David
    Build GitHub Actions
    License GitHub

    Table of Contents

    Installation

    $ npm install defekt

    Using defekt

    Creating custom errors

    To create custom errors, create new classes and let them extend the anonymous class created by defekt:

    import { defekt } from 'defekt';
    
    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    class TokenExpired extends defekt({ code: 'TokenExpired' }) {}

    The code identifies the error and can be used to differ between various types of errors.

    You may set a defaultMessage that is used when displaying the error. If you don't set a defaultMessage, a human-readable version of the code is used:

    import { defekt } from 'defekt';
    
    class TokenMalformed extends defekt({
      code: 'TokenMalformed',
      defaultMessage: 'The token is malformed.'
    }) {}
    
    class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
    
    const tokenMalformed = new TokenMalformed();
    const tokenExpired = new TokenExpired();
    
    console.log(tokenMalformed.message);
    // => 'The token is malformed.'
    
    console.log(tokenExpired.message);
    // => 'Token expired.'

    These custom errors can be used in various ways. They are, however, preferred to be passed around as objects, preferably wrapped in a Result type, instead of being thrown. This allows the handling of recoverable errors in a type-safe way, instead of using unchecked and unpredictable thrown exceptions or rejections.

    import { defekt, Result } from 'defekt';
    
    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
    
    const validateToken = function (token: string): Result<DecodedToken, TokenMalformed | TokenExpired> {
      // ...
    };
    
    const tokenResult = validateToken(rawToken);
    
    if (tokenResult.hasError()) {
      const { error } = tokenResult;
      
      switch (error.code) {
        // TypeScript will support you here and only allow the codes of the two possible errors.
        case TokenMalformed.code: {
          // ...
        }
        case TokenExpired.code: {
          // ...
        }
      }
    }

    Instantiating errors

    The custom errors created by this package take several parameters. They provide a default message, but you can override it:

    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    
    const error = new TokenMalformed('Token is not valid JSON.');

    You can instead provide an object, which can contain an optional cause for the error or additional data:

    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    
    try {
      // ...
    } catch (ex: unknown) {
      const error = new TokenMalformed({ cause: ex });
      // ...
    }
    
    const error = new TokenMalformed({ data: { foo: 'bar' }});

    Serializing, Deserializing and Hydrating errors

    Sometimes you need to serialize and deserialize your errors. Afterwards they are missing their prototype-chain and Error-related functionality. To restore those, you can hydrate a raw object to a CustomError-instance:

    import { defekt, hydrateCustomError } from 'defekt';
    
    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    
    const serializedTokenMalformedError = JSON.stringify(new TokenMalformed());
    
    const rawEx = JSON.parse(serializedTokenMalformedError);
    
    const ex = hydrateCustomError({ rawEx, potentialErrorConstructors: [ TokenMalformed ] }).unwrapOrThrow();

    Note that the hydrated error is wrapped in a Result. If the raw error can not be hydrated using one of the given potential error constructors, an error-Result will be returned, which tells you, why the hydration was unsuccessful. Also note that the cause of a CustomError is currently not hydrated, but left as-is.

    Usually, JavaScript Errors are not well suited for JSON-serialization. To improve this, the CustomError class implements toJSON(), which defines custom JSON-serialization behavior. If you want to be able to serialize your cause and data as well, they need to be either plain objects or also implement the toJSON method.

    Using custom error type-guards

    Custom errors can be type-guarded using isCustomError. With only one parameter it specifies an error's type to CustomError:

    try {
      // ...
    } catch (ex: unknown) {
      if (isCustomError(error)) {
        // In this scope error is of type CustomError.
      }
      // ...
    }

    You can supply the specific custom error constructor you want to check for as the second parameter:

    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    
    try {
      // ...
    } catch (ex: unknown) {
      if (isCustomError(error, TokenMalformed)) {
        // In this scope error is of type CustomError<'TokenMalformed'>.
        // This is usually functionally equivalent to the type TokenMalformed,
        // but has slight differences if e.g. you define properties on the
        // TokenMalformed class.
      }
      // ...
    }

    Using Result

    What is this and why would I use it

    Error handling is an integral part of reliable applications. Unfortunately, TypeScript does not provide a way to type-check exceptions or to even annotate functions with information about the exceptions they might throw. This makes all exceptions in TypeScript unchecked, unpredictable, and unreliable.

    In addition to that, JavaScript - and, by extension, TypeScript - does not differentiate between recoverable errors and unrecoverable errors. We recommend the blog post The Error Model by Joe Duffy on the differences between the two kinds of errors and various ways to implement them.

    This library aims to differentiate between recoverable errors and unrecoverable ones, by wrapping recoverable errors in a data structure. This approach is a more fancy version of the basic concept of error codes. Wrapping errors in data structures that have semantics is an attempt to bring concepts from languages like Haskell into TypeScript. Consider this situation:

    const configuration = await loadConfiguration();
    
    await startServer(configuration.port ?? 3000);

    Here, loadConfiguration might fail for several reasons. It might try to access the files system and fail because the configuration file does not exist. It might also fail because the configuration file is too large and the process runs out of memory. The former you want to handle since there is a default value for the port. The latter you don't want to handle, since you can't do anything about it. So imagine loadConfiguration would announce its recoverable errors in its signature:

    import fs from 'fs';
    import { error, value } from 'defekt';
    
    const loadConfiguration = async function (): Promise<Result<Configuration, ConfigurationNotFound>> {
      try {
        return value(
          JSON.parse(
            await fs.promises.readFile(configFilePath, 'utf8')
          )
        );
      } catch (ex) {
        if (ex.code === 'ENOENT') {
          return error(new ConfigurationNotFound({
            message: 'Failed to read configuration file.', 
            cause: ex
          }));
        }
        
        throw ex;
      }
    };
    
    const configuration = (await loadConfiguration()).unwrapOrDefault({ port: 3000 });
    
    await startServer(configuration.port);

    Here, any errors related to the configuration file missing are caught, propagated, and handled explicitly. If JSON.parse fails or if the process runs out of memory, an exception will be thrown and not be handled.

    Understanding the API

    There are two ways to construct a Result. A result can either be a ResultValue or a ResultError:

    import { error, value, Result } from 'defekt';
    
    const errorResult: Result<unknown, Error> = error(new Error());
    const valueResult: Result<number, unknown> = value(5);
    
    // Both are assignable to a Result with matching type parameters.
    let result = Result<number, Error>;
    
    result = valueResult;
    result = errorResult;

    When you get a result from a function, you can check whether it has failed and act appropriately:

    import { Result } from 'defekt';
    
    const someResult: Result<number, Error> = calculateStuff();
    
    if (someResult.hasError()) {
      // Propagate the error so that callers may handle it.
      return someResult;
    }
    
    console.log(someResult.value);

    Alternatively you can use hasValue to achieve the opposite.

    There is a more convenient solution, if you don't need to propagate your errors:

    import { Result } from 'defekt';
    
    class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
    class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
    
    const validateToken = function (token: string): Result<DecodedToken, TokenMalformed | TokenExpired> {
      // ...
    };
    
    const token = validateToken('a token').unwrapOrDefault({ sub: 'anonymous' });
    
    // Or, if you can't handle the possible errors appropriately and 
    // instead want to throw the error, possibly crashing your application:
    const token = validateToken('a token').unwrapOrThrow();
    
    // If you want to transform the error to add additional information, or to
    // fulfill a more general error type, you can also pass a callback:
    const token = validateToken('a token').unwrapOrThrow(
      err => new BroaderError({ message: 'Something went wrong', cause: err })
    );
    
    // If you want to handle errors by returning a conditional default 
    // value, you can use `unwrapOrElse` to supply a handler:
    const token = validateToken('a token').unwrapOrElse(
      (ex) => {
        switch (ex.code) {
          case TokenMalformed.code: {
            return { sub: 'anonymous', reason: 'malformed' };
          }
          case TokenExpired.code: {
            return { sub: 'anonymous', reason: 'expired' };
          }
        }
      }
    );

    Recognizing a Result

    If you need to assert the type of a Result, you can use the isResult type-guard:

    import { isResult } from 'defekt';
    
    const someValue: any = someFunction();
    
    if (isResult(someValue)) {
      // In this scope someValue is of type Result<any, any>.
    }

    Hydrating a Result

    Like for errors, there is a function to hydrate a Result from raw data in case you need to serialize and deserialize a Result.

    import { defekt, hydrateResult } from 'defekt';
    
    const rawResult = JSON.parse(resultFromSomewhere);
    
    const hydrationResult = hydrateResult({ rawResult });
    
    if (hydrationResult.hasError()) {
        // The hydration has failed.
    } else {
        const result = hydrationResult.value;
        
        if (result.hasError()) {
            // Continue with your normal error handling.
        }
    }

    You can also optionally let hydrateResult hydrate the contained error by passing potentialErrorConstructors. This works identically to hydrateResult.

    Running quality assurance

    To run quality assurance for this module use roboter:

    $ npx roboter

    Install

    npm i defekt

    DownloadsWeekly Downloads

    8,256

    Version

    9.1.1

    License

    MIT

    Unpacked Size

    51.7 kB

    Total Files

    42

    Last publish

    Collaborators

    • thenativeweb-admin
    • goloroden
    • yeldir
    • nhummel
    • dotkuro