Intro

TypeORM is a really nice ORM layer that utilizes TypeScript features to provide a superior development experience. Coming from Djangoland where Django ORM is a bliss to work with, TypeORM seems like the closest alternative in the JavaScript ecosystem. TypeORM wraps database-specific errors QueryFailedError object. Not all database errors are non-recoverable. One such error can be unique-constraint where you can ask user to provide a different value if triggered. This is obviously better than returning 500 error response and leaving the user in a state of mental confusion riddled with guilt on what possibly wrong request they could have made. This article shows you how to catch unique-constraint errors in a reliable fashion.

Creating new object

To create a new object in TypeORM, you do something like:

import { getRepository } from "typeorm";
import { Book } from "db/entity/Book";

const bookRepo = getRepository(Book);
const book = bookRepo.create({...data})

try {
    await bookRepo.save(book);
} catch (err) {
    console.log(err)
}

Catching Unique Constraint Errors in PostgreSQL

For this to work, you need to setup database-level unique constraints like:

import { Entity, Unique, BaseEntity }  from "typeorm";

@Entity()
@Unique(["title", "author"])
export default class Book extends BaseEntity {
....

PostgreSQL supports unique on multiple columns. So, unique constraint is triggered if you try adding a new row with  values for title and author that both matches an existing row.

TypeORM doesn't convert database/driver specific errors codes to standard set of errors. Instead, on error, it exposes all error properties exposed by the underlying driver. This is confirmed by the following TypeORM's code:

import { ObjectUtils } from "../util/ObjectUtils";

/**
 * Thrown when query execution has failed.
*/
export class QueryFailedError extends Error {

    constructor(query: string, parameters: any[]|undefined, driverError: any) {
        super();
        Object.setPrototypeOf(this, QueryFailedError.prototype);
        this.message = driverError.toString()
            .replace(/^error: /, "")
            .replace(/^Error: /, "")
            .replace(/^Request/, "");
        ObjectUtils.assign(this, {
            ...driverError,
            name: "QueryFailedError",
            query: query,
            parameters: parameters || []
        });
    }

}

If you look at QueryFailedError, you see that driverError is spread out into the object. The driver for PostgreSQL is node-postgres which has an error object like:

 {
  name: 'error',
  severity: 'ERROR',
  code: '23505',
  detail: 'Key (title, author)=(1984, George Orwell) already exists." 
}

The code is a PostgreSQL Error Code and all of which is documented here:

https://www.postgresql.org/docs/10/errcodes-appendix.html

Hence, to catch unique constraint errors, we can do something like:

import { getRepository } from "typeorm";
import { Book } from "db/entity/Book";

const PG_UNIQUE_CONSTRAINT_VIOLATION = "23505";

// validate data here, maybe use json schemas for validation

const bookRepo = getRepository(Book);
const book = bookRepo.create({ ...validatedData });

try {
  await bookRepo.save(book);
} catch (err) {
  if (err && err.code === PG_UNIQUE_CONSTRAINT_VIOLATION) {
    // maybe send BAD_REQUEST to client
  } else {
    // if data was already validated, this could be a server error
    // send INTERNAL_SERVER_ERROR to client
  }
}

In Koa, I do something very similar:

import ajv from "ajv";
import HttpStatus from "http-status-codes";
import Router from "koa-router";
import { getRepository } from "typeorm";

import Book from 'db/entity/Book';
import bookSchema from 'schemas/entity/Book'

const PG_UNIQUE_CONSTRAINT_VIOLATION = "23505";

const router = new Router({ prefix: "/api" });

router.post("/books", async (ctx, _) => {
  const validator = new ajv({ removeAdditional: true });
  const valid = validator.validate(bookSchema, ctx.request.body);
  if (!valid) {
    ctx.status = HttpStatus.BAD_REQUEST;
    ctx.body = { errors: validator.errorsText() };
    return;
  }

  const bookRepo = getRepository(Book);
  const book = bookRepo.create({ ...ctx.request.body });
  try {
    ctx.body = await bookRepo.save(book);
    ctx.status = HttpStatus.CREATED;
  } catch (err) {
    if (err && err.code === PG_UNIQUE_CONSTRAINT_VIOLATION) {
      ctx.body = { errors: err.detail };
      ctx.status = HttpStatus.BAD_REQUEST;
      return;
    }
    ctx.status = HttpStatus.INTERNAL_SERVER_ERROR;
    throw err;
  }
});

Here, I am first running the data through a JSON Schema Validator (ajv). It's obvious that JSON Schemas alone cannot catch database level errors. Hence, I catch possible errors when saving and try to handle them if possible. In this case, user will now get hint that the book's values for unique fields already exist and they will have to try another set for values.