Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/site/reference/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,45 @@ You have the following options how to fix the error
}
```

## Database Error Codes

When repository operations fail due to database constraints, connection issues,
or protocol errors, LoopBack maps low-level database driver errors to
standardized, protocol-neutral error codes.

These domain error codes are automatically mapped to corresponding REST HTTP
statuses by `@loopback/rest`:

| Error Code | HTTP Status | Description |
| :---------------------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `UNIQUE_CONSTRAINT_VIOLATION` | 409 Conflict | A record with a duplicate key or non-unique value was inserted or updated. |
| `LOCK_CONFLICT` | 409 Conflict | The operation failed due to a database deadlock or lock wait timeout. |
| `FOREIGN_KEY_VIOLATION` | 422 Unprocessable Entity | The operation violates a foreign key constraint (referencing a non-existent parent or deleting a referenced parent). |
| `NOT_NULL_VIOLATION` | 400 Bad Request | A required non-nullable database column was provided as `null` or omitted without a default value. |
| `CHECK_CONSTRAINT_VIOLATION` | 400 Bad Request | The operation violates a custom SQL `CHECK` constraint. |
| `DATA_TYPE_MISMATCH` | 400 Bad Request | A value provided does not match the database column type (e.g., string length exceeded, invalid string format for field type). |
| `GENERATED_COLUMN_VIOLATION` | 400 Bad Request | An explicit attempt was made to write or update a generated/computed database column. |
| `QUERY_TIMEOUT` | 504 Gateway Timeout | The database query execution exceeded the configured statement timeout threshold. |
| `CONNECTION_FAILURE` | 503 Service Unavailable | The database connection was lost, refused, or unable to be acquired from the pool. |

### Configuring or Disabling Database Error Mapping

By default, `@loopback/rest` automatically translates database domain errors
into HTTP error responses. You can disable this mapping across your application
using `RestBindings.DATABASE_ERROR_MAPPING_OPTIONS`:

```typescript
import {RestBindings} from '@loopback/rest';

// In your application constructor or sequence setup
app.bind(RestBindings.DATABASE_ERROR_MAPPING_OPTIONS).to({
enabled: false,
});
```

When set to `{ enabled: false }`, database errors pass through unmapped and fall
back to the default error handler (returning an HTTP 500 status).

## Other error codes

Besides LoopBack-specific error codes, your application can encounter low-level
Expand Down
2 changes: 1 addition & 1 deletion examples/todo/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function givenTodo(todo?: Partial<Todo>) {

export const aLocation = {
address: '1 New Orchard Road, Armonk, 10504',
geopoint: <GeoPoint>{y: 41.109728357749, x: -73.72462031805},
geopoint: <GeoPoint>{y: 41.109725723771, x: -73.724620709372},
get geostring() {
return `${this.geopoint.y},${this.geopoint.x}`;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';

import {
CheckConstraintError,
ColumnNotFoundError,
DatabaseConnectionError,
DatabaseError,
DataTypeMismatchError,
ForeignKeyConstraintError,
GeneratedColumnViolationError,
LockConflictError,
NotNullConstraintError,
QueryTimeoutError,
DbSyntaxError,
EntityNotFoundError,
UniqueConstraintError,
} from '../../..';
import {handleRepositoryError} from '../../../errors/handle-repository-error';

describe('handleRepositoryError', () => {
it('throws DatabaseError for a falsy error', () => {
expect(() => handleRepositoryError(null)).to.throwError(
new DatabaseError('An unknown database execution error occurred.'),
);
});

it('passes through an existing DatabaseError', () => {
const error = new DatabaseError('Database error');

expect(() => handleRepositoryError(error)).to.throwError(error);
});

it('maps UNIQUE_CONSTRAINT_VIOLATION', () => {
const error = givenConnectorError(
'UNIQUE_CONSTRAINT_VIOLATION',
'Duplicate value',
);

expect(() => handleRepositoryError(error)).to.throwError(
UniqueConstraintError,
);

expect(() => handleRepositoryError(error)).to.throwError(/Duplicate value/);
});

it('maps FOREIGN_KEY_VIOLATION', () => {
const error = givenConnectorError(
'FOREIGN_KEY_VIOLATION',
'Foreign key violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
ForeignKeyConstraintError,
);
});

it('maps NOT_NULL_VIOLATION', () => {
const error = givenConnectorError(
'NOT_NULL_VIOLATION',
'Not-null violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
NotNullConstraintError,
);
});

it('maps CHECK_CONSTRAINT_VIOLATION', () => {
const error = givenConnectorError(
'CHECK_CONSTRAINT_VIOLATION',
'Check constraint violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
CheckConstraintError,
);
});

it('maps TABLE_NOT_FOUND', () => {
const error = givenConnectorError('TABLE_NOT_FOUND', 'Entity not found');

expect(() => handleRepositoryError(error)).to.throwError(
EntityNotFoundError,
);
});

it('maps COLUMN_NOT_FOUND', () => {
const error = givenConnectorError('COLUMN_NOT_FOUND', 'Column not found');

expect(() => handleRepositoryError(error)).to.throwError(
ColumnNotFoundError,
);
});

it('maps GENERATED_COLUMN_VIOLATION', () => {
const error = givenConnectorError(
'GENERATED_COLUMN_VIOLATION',
'Generated column violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
GeneratedColumnViolationError,
);
});

it('maps DATA_TYPE_MISMATCH', () => {
const error = givenConnectorError(
'DATA_TYPE_MISMATCH',
'Data type mismatch',
);

expect(() => handleRepositoryError(error)).to.throwError(
DataTypeMismatchError,
);
});

it('maps SYNTAX_ERROR', () => {
const error = givenConnectorError('SyntaxError', 'Syntax error');
expect(() => handleRepositoryError(error)).to.throwError(DbSyntaxError);
});

it('maps QUERY_TIMEOUT', () => {
const error = givenConnectorError('QUERY_TIMEOUT', 'Query timeout');
expect(() => handleRepositoryError(error)).to.throwError(QueryTimeoutError);
});

it('maps LOCK_CONFLICT', () => {
const error = givenConnectorError('LOCK_CONFLICT', 'Lock conflict');

expect(() => handleRepositoryError(error)).to.throwError(LockConflictError);
});

it('maps CONNECTION_FAILURE', () => {
const error = givenConnectorError(
'CONNECTION_FAILURE',
'Connection failure',
);

expect(() => handleRepositoryError(error)).to.throwError(
DatabaseConnectionError,
);
});

it('preserves message and details when mapping an error', () => {
const details = {
constraint: 'users_email_key',
field: 'email',
};

const error = {
code: 'UNIQUE_CONSTRAINT_VIOLATION',
message: 'Email already exists',
details,
};

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.be.instanceof(UniqueConstraintError);
expect((err as UniqueConstraintError).message).to.equal(
'Email already exists',
);
expect((err as UniqueConstraintError).details).to.eql(details);
}
});

it('rethrows an unmapped connector error as-is', () => {
const error = {
code: 'SOME_UNKNOWN_ERROR',
message: 'Unknown connector error',
};

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.equal(error);
}
});

it('rethrows a generic Error as-is', () => {
const error = new Error('Something went wrong');

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.equal(error);
}
});

it('rethrows an object without a code as-is', () => {
const error = {
message: 'Something went wrong',
};

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.equal(error);
}
});
});

function givenConnectorError(code: string, message: string) {
return {
code,
message,
details: {
source: 'test',
},
};
}
Loading
Loading