Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adjust checking for internal and public errors #153

Merged
merged 6 commits into from
Sep 2, 2024
Merged
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
4 changes: 2 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ export type { ErrorDetails } from './src/errors/types'

export {
PublicNonRecoverableError,
isPublicNonRecoverableError,
type PublicNonRecoverableErrorParams,
} from './src/errors/PublicNonRecoverableError'

export {
InternalError,
isInternalError,
type InternalErrorParams,
} from './src/errors/InternalError'
export { isEntityGoneError } from './src/errors/errorTypeGuards'
Expand Down Expand Up @@ -48,10 +50,8 @@ export {

export {
isError,
isInternalError,
isStandardizedError,
isObject,
isPublicNonRecoverableError,
hasMessage,
} from './src/utils/typeUtils'
export { type StandardizedError } from './src/utils/typeUtils'
Expand Down
6 changes: 1 addition & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
"url": "git://github.com/lokalise/node-core.git"
},
"license": "Apache-2.0",
"files": [
"dist/**",
"LICENSE",
"README.md"
],
"files": ["dist/**", "LICENSE", "README.md"],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "commonjs",
Expand Down
34 changes: 34 additions & 0 deletions src/errors/InternalError.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { InternalError, isInternalError } from './InternalError'

describe('InternalError', () => {
describe('isInternalError', () => {
it('returns false for native Error', () => {
const err = new Error('Unknown')

expect(isInternalError(err)).toBe(false)
})

it('detects if error is internal', () => {
const err = new InternalError({
message: 'Unknown',
errorCode: 'INTERNAL_ERROR',
})

expect(isInternalError(err)).toBe(true)
})

it('detects if error is public for extended error', () => {
class ExtendedInternalError extends InternalError {
constructor() {
super({
message: 'Unknown',
errorCode: 'INTERNAL_ERROR',
})
}
}
const err = new ExtendedInternalError()

expect(isInternalError(err)).toBe(true)
})
})
})
17 changes: 16 additions & 1 deletion src/errors/InternalError.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isError } from '../utils/typeUtils'
import type { ErrorDetails } from './types'

export type InternalErrorParams<T = ErrorDetails> = {
Expand All @@ -7,16 +8,30 @@ export type InternalErrorParams<T = ErrorDetails> = {
cause?: unknown
}

const INTERNAL_ERROR_SYMBOL_KEY = 'INTERNAL_ERROR_KEY'
const internalErrorSymbol = Symbol.for(INTERNAL_ERROR_SYMBOL_KEY)

export class InternalError<T = ErrorDetails> extends Error {
readonly [internalErrorSymbol] = true
public readonly details?: T
public readonly errorCode: string

constructor(params: InternalErrorParams<T>) {
super(params.message, {
cause: params.cause,
})
this.name = 'InternalError'
// set the name as the class name for every class that extends InternalError
this.name = this.constructor.name
this.details = params.details
this.errorCode = params.errorCode
}
}

export function isInternalError(error: unknown): error is InternalError {
return (
isError(error) &&
// biome-ignore lint/suspicious/noExplicitAny: checking for existence of prop outside or Error interface
((error as any)[Symbol.for(INTERNAL_ERROR_SYMBOL_KEY)] === true ||
error.name === 'InternalError')
)
}
35 changes: 33 additions & 2 deletions src/errors/PublicNonRecoverableError.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import { PublicNonRecoverableError } from './PublicNonRecoverableError'
import { PublicNonRecoverableError, isPublicNonRecoverableError } from './PublicNonRecoverableError'

describe('PublicNonRecoverableError', () => {
it('sets http status code to 500 by default', () => {
const error = new PublicNonRecoverableError({
message: 'error',
message: 'Unknown',
errorCode: 'PUBLIC_ERROR',
})

expect(error.httpStatusCode).toBe(500)
})

describe('isPublicNonRecoverableError', () => {
it('returns false for native Error', () => {
const err = new Error('Unknown')

expect(isPublicNonRecoverableError(err)).toBe(false)
})

it('detects if error is public', () => {
const err = new PublicNonRecoverableError({
message: 'Unknown',
errorCode: 'PUBLIC_ERROR',
})

expect(isPublicNonRecoverableError(err)).toBe(true)
})

it('detects if error is public for extended error', () => {
class ExtendedPublicNonRecoverableError extends PublicNonRecoverableError {
constructor() {
super({
message: 'Unknown',
errorCode: 'PUBLIC_ERROR',
})
}
}
const err = new ExtendedPublicNonRecoverableError()

expect(isPublicNonRecoverableError(err)).toBe(true)
})
})
})
17 changes: 16 additions & 1 deletion src/errors/PublicNonRecoverableError.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isError } from '../utils/typeUtils'
import type { ErrorDetails } from './types'

export type PublicNonRecoverableErrorParams<T = ErrorDetails> = {
Expand All @@ -8,10 +9,14 @@ export type PublicNonRecoverableErrorParams<T = ErrorDetails> = {
cause?: unknown
}

const PUBLIC_NON_RECOVERABLE_ERROR_SYMBOL_KEY = 'PUBLIC_NON_RECOVERABLE_ERROR_KEY'
const publicNonRecoverableErrorSymbol = Symbol.for(PUBLIC_NON_RECOVERABLE_ERROR_SYMBOL_KEY)

/**
* This error is returned to the consumer of API
*/
export class PublicNonRecoverableError<T = ErrorDetails> extends Error {
readonly [publicNonRecoverableErrorSymbol] = true
public readonly details?: T
public readonly errorCode: string
public readonly httpStatusCode: number
Expand All @@ -20,9 +25,19 @@ export class PublicNonRecoverableError<T = ErrorDetails> extends Error {
super(params.message, {
cause: params.cause,
})
this.name = 'PublicNonRecoverableError'
// set the name as the class name for every class that extends PublicNonRecoverableError
this.name = this.constructor.name
this.details = params.details
this.errorCode = params.errorCode
this.httpStatusCode = params.httpStatusCode ?? 500
}
}

export function isPublicNonRecoverableError(error: unknown): error is PublicNonRecoverableError {
return (
isError(error) &&
// biome-ignore lint/suspicious/noExplicitAny: checking for existence of prop outside or Error interface
((error as any)[Symbol.for(PUBLIC_NON_RECOVERABLE_ERROR_SYMBOL_KEY)] === true ||
error.name === 'PublicNonRecoverableError')
)
}
2 changes: 1 addition & 1 deletion src/errors/errorTypeGuards.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPublicNonRecoverableError } from '../utils/typeUtils'
import { isPublicNonRecoverableError } from '../errors/PublicNonRecoverableError'

import type { EntityGoneError } from './publicErrors'

Expand Down
49 changes: 1 addition & 48 deletions src/utils/typeUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { InternalError } from '../errors/InternalError'
import { PublicNonRecoverableError } from '../errors/PublicNonRecoverableError'

import {
hasMessage,
isError,
isInternalError,
isObject,
isPublicNonRecoverableError,
isStandardizedError,
} from './typeUtils'
import { hasMessage, isError, isObject, isStandardizedError } from './typeUtils'

describe('typeUtils', () => {
describe('isError', () => {
Expand Down Expand Up @@ -55,46 +48,6 @@ describe('typeUtils', () => {
})
})

describe('isInternalError', () => {
it('true for InternalError', () => {
const error = new InternalError({
message: 'dummy',
errorCode: 'code',
})

expect(isInternalError(error)).toBe(true)
})

it('false for PublicNonRecoverableError', () => {
const error = new PublicNonRecoverableError({
message: 'dummy',
errorCode: 'code',
})

expect(isInternalError(error)).toBe(false)
})
})

describe('isPublicNonRecoverableError', () => {
it('false for InternalError', () => {
const error = new InternalError({
message: 'dummy',
errorCode: 'code',
})

expect(isPublicNonRecoverableError(error)).toBe(false)
})

it('true for PublicNonRecoverableError', () => {
const error = new PublicNonRecoverableError({
message: 'dummy',
errorCode: 'code',
})

expect(isPublicNonRecoverableError(error)).toBe(true)
})
})

describe('hasMessage', () => {
it('true for something with message', () => {
const error = new InternalError({
Expand Down
11 changes: 0 additions & 11 deletions src/utils/typeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import type { InternalError } from '../errors/InternalError'
import type { PublicNonRecoverableError } from '../errors/PublicNonRecoverableError'

// Error structure commonly used in libraries, e. g. fastify
export type StandardizedError = {
code: string
Expand All @@ -19,16 +16,8 @@ export function isStandardizedError(error: unknown): error is StandardizedError
return isObject(error) && typeof error.code === 'string' && typeof error.message === 'string'
}

export function isInternalError(error: unknown): error is InternalError {
return isObject(error) && error.name === 'InternalError'
}

export function isError(maybeError: unknown): maybeError is Error {
return (
maybeError instanceof Error || Object.prototype.toString.call(maybeError) === '[object Error]'
)
}

export function isPublicNonRecoverableError(error: unknown): error is PublicNonRecoverableError {
return isObject(error) && error.name === 'PublicNonRecoverableError'
}
Loading