Creating error data

The error data depends on the error code provided by the database.

We should identify the code given by Postgres to set an appropriate httpError and also a description of the situation. Let's return to the HttpError to add one more possible error.

CONFLICT: {
  status: HttpStatus.CONFLICT,
  error: 'Conflict',
},

Back in the filter, let's create some constants to help us with the specific error codes and corresponding descriptions. Also, there is one postgres error code that can represent two of the three cases we're handling. Therefore, when it appears, we'll have to check the error message itself in order to understand what's the case.

private readonly DatabaseErrorCode = {
  ASSOCIATION_NOT_FOUND_OR_NOT_NULL_VIOLATION: '23503',
  UNIQUE_VIOLATION: '23505',
} as const satisfies Record<string, string>;

private readonly MessageSnippet = {
  ASSOCIATION_NOT_FOUND: 'is not present',
  NOT_NULL_VIOLATION: 'is still referenced',
} as const satisfies Record<string, string>;

private readonly Description = {
  ASSOCIATION_NOT_FOUND: 'Associated entity not found',
  NOT_NULL_VIOLATION: 'Cannot delete due to NOT NULL constraint',
  UNIQUE_VIOLATION: 'Unique constraint violation',
} as const satisfies Record<string, string>;

The next step is to create a method that will receive the error code and the message, and then return an httpError and a description. It may seem like a lot of code but this is simply a switch, nothing too complicated. We don't use the default because, if an unspecified case occurs, we want the httpError to be undefined. We'll use this to know the BaseExceptionFilter should be invoked.

private createErrorData(code: string, message: string) {
  let httpError: any;
  let description: string;

  switch (code) {
    case this.DatabaseErrorCode.ASSOCIATION_NOT_FOUND_OR_NOT_NULL_VIOLATION:
      if (message.includes(this.MessageSnippet.ASSOCIATION_NOT_FOUND)) {
        httpError = HttpError.NOT_FOUND;
        description = this.Description.ASSOCIATION_NOT_FOUND;
      } else if (message.includes(this.MessageSnippet.NOT_NULL_VIOLATION)) {
        httpError = HttpError.CONFLICT;
        description = this.Description.NOT_NULL_VIOLATION;
      }
      break;
    case this.DatabaseErrorCode.UNIQUE_VIOLATION:
      httpError = HttpError.CONFLICT;
      description = this.Description.UNIQUE_VIOLATION;
      break;
  }

  return { httpError, description };
}

You may have noticed something unpleasant: the httpError has the type any. There is an interesting way to have a better type here. Back in the http-error.util file, a type may be created to represent one of the fields of HttpError like this:

export type HttpError = (typeof HttpError)[keyof typeof HttpError];

Now, to explain the code above: first, we use typeof HttpError to represent its type. But, as we want a type for one of its fields, instead of itself, we use square brackets ([]) to indicate we want the type of a field. Well, we don't want a specific field, but any field. To achieve this, we use keyof in combination with typeof HttpError once again, which results in the union of all the fields' names of the HttpError.

This type can now be applied. Even though the constant and the type have the same name, the IDE can distinguish when one or the other is being used.

let httpError: HttpError;

Finally, we just need to perform last steps, mostly known to us.

Last updated