0.0.25 • Published 4 months ago

application-exception v0.0.25

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

Application Exception

Jest coverage Strictest TypeScript Config Package License MIT Npm Version

Warning Please use fixed version (remove ^ from package.json).

Motivation

  • Error object must have more default props than just message and stack.
  • Extending Error object with custom props must be convenient.
  • Error object must allow to specify a list of nested root causes.
  • Error object must have a consistent JSON representation.
  • Informative error messages must be easy to create.

User Guide

Defaults of ApplicationException

By default ApplicationException sets id, timestamp and message fields.

(Run with npm run ts-file ./examples/default-fields-example.ts or see example's source code)

const e = AppEx.new();

console.log('id:'.padEnd(10), e.getId());
console.log('timestamp:'.padEnd(10), e.getTimestamp());
console.log('message:'.padEnd(10), e.getMessage());

prints

Id:        AE_00Q6F4K3K7FPYQNEFJA1GV465Z
Timestamp: 2023-01-08T02:45:12.309Z
Message:   Something went wrong

You can provide a message string to ApplicationException.new.

(Run with npm run ts-file ./examples/default-fields-with-custom-message-example.ts or see example's source code)

const e = AppEx.new(`I'm an error message`);

console.log('message:'.padEnd(10), e.getMessage());

prints

message:   I'm an error message

Using builder pattern

All fields available with builder pattern are listed in AppExOwnProps type.

You can use builder methods to set all of these fields except for message field because once message is set on Error instance it is impossible to change it. One time when setting message is available is during object creation.

Here is a simple example.

(Run with npm run ts-file ./examples/builder-pattern-simple-example.ts or see example's source code)

throw AppEx.new(`Could not fetch a resource with id {{id}}`)
  .numCode(404)
  .code('RESOURCE_NOT_FOUND')
  .details({ id });

This is a more complicated example demonstrating more fields.

(Run with npm run ts-file ./examples/builder-pattern-example.ts or see example's source code)

function addUser(email: string): void {
  try {
    storeUser(email);
  } catch (caught) {
    if (caught instanceof Error && caught.message === 'User already exists') {
      throw AppEx.new(`User with this email already exists - {{email}}`)
        .displayMessage(
          'We already have a user with this email in the system, maybe you signed up earlier?',
        )
        .code('USER_ALREADY_EXISTS')
        .numCode(400)
        .causedBy(caught)
        .details({ email });
    } else {
      throw AppEx.new('Could not create user')
        .displayMessage('Something went wrong, please visit help center')
        .numCode(500)
        .causedBy(caught)
        .details({ email });
    }
  }
}

Using default static method constructors

ApplicationException.new is the simplest constructor variant.

Other constructors available by default are lines and prefixedLines (or plines).

(Run with npm run ts-file ./examples/constructor-variants-example.ts or see example's source code)

/**
 * `lines` joins all string arguments with '\n'
 */
const e1 = AppEx.lines(
  'Could not fetch user from ThirdParty',
  `- HTTP             - GET https://example.org/api/v1`,
  `- Request headers  - {{{json req.headers}}}`,
  `- Response status  - {{{json res.status}}}`,
  `- Response headers - {{{json res.headers}}}`,
).details({ req, res });

/**
 * Same as `lines`, but adds a prefix to all line arguments.
 */
const e2 = AppEx.prefixedLines(
  'UserService.getUser',
  'Could not fetch user from ThirdParty',
  `- HTTP             - GET https://example.org/api/v1`,
  `- Request headers  - {{{json req.headers}}}`,
  `- Response status  - {{{json res.status}}}`,
  `- Response headers - {{{json res.headers}}}`,
).details({ req, res });

Templating

Fields message and displayMessage are actually Handlebars templates.

(Run with npm run ts-file ./examples/simple-templating-example.ts or see example's source code)

const e = AppEx.new('Bad thing happened').displayMessage(
  'Something went wrong, please contact tech support and provide this id - {{self.id}}',
);

console.log(e.getDisplayMessage());

prints

Something went wrong, please contact tech support and provide this id - AE_0DFG6FGFRCY2THPMMCNXAZF4KF

You can use fields specified in details on the top level. Use self to access exception object in handlebars template. self contains all fields available through builder methods on it's top level, like id or code. Also, there are several handlebars helper functions available

  • json
  • pad-end
  • pad-start

All compilation context available is presented in the following example.

(Run with npm run ts-file ./examples/all-templating-helpers-example.ts or see example's source code)

const e = AppEx.new('Bad thing happened')
  .details({
    a: 12345,
    b: 'b-field',
  })
  .displayMessageLines(
    'top level fields',
    '- a - {{a}}',
    '- b - {{b}}',
    'self',
    '- self.id - {{self.id}}',
    '- self.timestamp - {{self.timestamp}}',
    '- self.code - {{self.code}}',
    '- self.numCode - {{self.numCode}}',
    '- self.constructor_name - {{self.constructor_name}}',
    'helpers',
    '- self.id end padded 1   - padding start ->{{pad-end 40 self.id}}<- padding end',
    '- self.id end padded 2   - padding start ->{{pad-end 40 "-" self.id}}<- padding end',
    '- self.id start padded 1 - padding start ->{{pad-start 40 self.id}}<- padding end',
    '- self.id start padded 2 - padding start ->{{pad-start 40 "-" self.id}}<- padding end',
    '- self.details - {{{json self.details}}}',
    '- self.details indented -',
    '{{{json self.details 4}}}',
  );

Custom exceptions: Extending ApplicationException class

Overriding static method defaults allows to specify default values of exception fields.

(Run with npm run ts-file ./examples/extending-class-example.ts or see example's source code)

class MyAppException extends ApplicationException {
  static override defaults(): ApplicationExceptionDefaultsProps {
    return {
      details({ now }) {
        return {
          value: {
            src: 'my-app-api-server',
            ts_in_ukraine: format(now, 'd MMMM yyyy, HH:mm:ss', {
              locale: localeUkraine,
            }),
          },
        };
      },
      useClassNameAsCode() {
        return { value: true };
      },
    };
  }

  static create(this: ApplicationExceptionStatic, num: number) {
    return this.new(
      'Creating from `create` static method. "num" is: {{num}}. Also "src" is set by default: {{src}}.',
    ).details({ num });
  }
}

You can still use new static method as a constructor like this

const e1 = MyAppException.new(
  'Using the default `new` constructor. "src" is set by default: {{src}}',
);
const e1Json = e1.toJSON();
delete e1Json.stack;
console.log(e1Json);

which prints

{
  constructor_name: 'MyAppException',
  message: 'Using the default `new` constructor. "src" is set by default: my-app-api-server',
  code: 'MyAppException',
  details: {
    src: 'my-app-api-server',
    ts_in_ukraine: '15 січня 2023, 05:04:46'
  },
  id: 'AE_TDHCSXDTETRSQFTSTS3QRF3SCQ',
  timestamp: '2023-01-15T03:04:46.173Z',
  raw_message: 'Using the default `new` constructor. "src" is set by default: {{src}}',
  v: 'appex/v0.1'
}

But you've also defined a create static constructor, that should be more suitable to intended calling context of MyAppException.

const e2 = MyAppException.create(21);
const e2Json = e2.toJSON();
delete e2Json.stack;
console.log(e2Json);

prints

{
  constructor_name: 'MyAppException',
  message: 'Creating from `create` static method. "num" is: 21. Also "src" is set by default: my-app-api-server.',
  code: 'MyAppException',
  details: {
    src: 'my-app-api-server',
    ts_in_ukraine: '15 січня 2023, 05:06:35',
    num: 21
  },
  id: 'AE_3RKF2423NK98JCH82Z25BDWBKR',
  timestamp: '2023-01-15T03:06:35.681Z',
  raw_message: 'Creating from `create` static method. "num" is: {{num}}. Also "src" is set by default: {{src}}.',
  v: 'appex/v0.1'
}

You can further extend MyAppException. MyServiceException inherits all instance and static methods and also inherits defaults. details field objects are merged (can be configured with mergeDetails option).

class MyServiceException extends MyAppException {
  static override defaults(): ApplicationExceptionDefaultsProps {
    return {
      details() {
        return { value: { scope: 'my-service' } };
      },
    };
  }
}

const e3 = MyServiceException.create(123);
const e3Json = e3.toJSON();
delete e3Json.stack;
console.log(e3Json);

prints

{
  constructor_name: 'MyServiceException',
  message: 'Creating from `create` static method. "num" is: 123. Also "src" is set by default: my-app-api-server.',
  code: 'MyServiceException',
  details: {
    src: 'my-app-api-server',
    ts_in_ukraine: '15 січня 2023, 05:07:41',
    scope: 'my-service',
    num: 123
  },
  id: 'AE_V535S4SK0W8RWARKR8DMVBJ5X1',
  timestamp: '2023-01-15T03:07:41.837Z',
  raw_message: 'Creating from `create` static method. "num" is: {{num}}. Also "src" is set by default: {{src}}.',
  v: 'appex/v0.1'
}

Custom exceptions: Using subclass static method

MyAppException is the same as in example from previous section except for ts_in_ukraine field, because details are not constructed dynamically.

(Run with npm run ts-file ./examples/subclass-example.ts or see example's source code)

const MyAppException = AppEx.subclass(
  'MyAppException',
  {
    useClassNameAsCode: true,
    details: {
      src: 'my-app-api-server',
    },
  },
  {
    create(this: ApplicationExceptionStatic, num: number) {
      return this.new(
        'Creating from `create` static method. "num" is: {{num}}. Also "src" is set by default: {{src}}.',
      ).details({ num });
    },
  },
);

subclass is good for quickly extending base versions because it is simple, but you will not be able to use it as a TypeScript type. The next piece of code shows how subclass of MyAppException created with subclass does not allow TypeScript to understand that it has a create static method available without extra code.

const MyServiceException = MyAppException.subclass(
  'MyServiceException',
  {
    details: {
      scope: 'my-service',
    },
  },
  /**
   * This is required for TypeScript to understand that `create` is available on `MyServiceException`
   */
  {
    create: MyAppException.create,
  },
);

Custom exceptions: Providing custom handlebars helpers

(Run with npm run ts-file ./examples/custom-handlebars-helpers.ts or see example's source code)

const MyAppException = AppEx.subclass(
  'MyAppException',
  {
    useClassNameAsCode: true,
    details: {
      src: 'my-app-api-server',
    },
    handlebarsHelpers: {
      'date-iso': function (...args: unknown[]): string {
        return new Date(args[0] as Date).toISOString();
      },
      'date-fmt': function (...args: unknown[]): string {
        return format(new Date(args[1] as Date), args[0] as string, {
          locale: localeUkraine,
        });
      },
    },
  },
  {
    defaults(
      this: ApplicationExceptionStatic,
    ): ApplicationExceptionDefaultsProps {
      return {
        details({ now }) {
          return {
            value: {
              ts_in_ukraine: format(now, 'd MMMM yyyy, HH:mm:ss', {
                locale: localeUkraine,
              }),
            },
          };
        },
      };
    },

    create(this: ApplicationExceptionStatic, num: number) {
      return this.new(
        '{{pad-end 20 self.constructor_name}} // ISO Date: {{date-iso self.timestamp}}; Formatted Date: {{date-fmt "d MMMM yyyy, HH:mm:ss" self.timestamp}}; num: {{num}}',
      ).details({ num });
    },
  },
);

const e = MyAppException.create(543231);

console.log(e.getMessage());

const MyServiceException = MyAppException.subclass('MyServiceException', {
  details() {
    return { value: { service: 'my-service' } };
  },
});

const e1 = MyServiceException.create(3098);

console.log(e1.getMessage());

prints

MyAppException       // ISO Date: 2023-01-15T03:23:00.647Z; Formatted Date: 15 січня 2023, 05:23:00; num: 543231
MyServiceException   // ISO Date: 2023-01-15T03:23:00.660Z; Formatted Date: 15 січня 2023, 05:23:00; num: 3098

Custom exceptions: Setting a type for details field

To assign a type to details field, you need to override setDetails method and make a function with assertion that it will return a subclass.

(Run with npm run ts-file ./examples/typing-details-field.ts or see example's source code)

type Details = {
  firstName: string;
  lastName: string;
};

class MyAppException extends ApplicationException {
  override setDetails(d: Details): this {
    return super.setDetails(d);
  }

  static create(): MyAppException {
    return new MyAppException(
      this.normalizeInstanceConfig({
        message: 'Hey, {{firstName}} {{lastName}}! You got a new exception!',
      }),
    );
  }
}

const e = MyAppException.create().code('HEY').details({
  firstName: 'Isaac',
  lastName: 'Newton',
});

console.log(e.getMessage());

This allows for code completion.

Details Field Webstorm Code Completion

And highlighting not allowed fields by TypeScript aware IDEs.

Details Field Webstorm TypeScript Error Highlighting

Consider not using throw

"Programs that use exceptions as part of their normal processing suffer from all the readability and maintainability problems of classic spaghetti code." — Andy Hunt, Dave Thomas - The Pragmatic Programmer

Consider using conventional control flow for exceptions handling. This means returning exception object from a function instead of throwing an exception object.

TODO: example (?)

API

Fields

TODO

Options

TODO

Helper methods (lifecycle methods)

TODO

Handlebars Helpers

  • pad
  • json

TODO

0.0.21

4 months ago

0.0.22

4 months ago

0.0.23

4 months ago

0.0.24

4 months ago

0.0.25

4 months ago

0.0.20

1 year ago

0.0.19

1 year ago

0.0.18

1 year ago

0.0.17

1 year ago

0.0.16

1 year ago

0.0.15

1 year ago

0.0.14

1 year ago

0.0.13

1 year ago

0.0.12

1 year ago

0.0.11

1 year ago

0.0.10

1 year ago

0.0.9

1 year ago

0.0.8

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.5

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago