new-error
A production-grade error creation library designed for Typescript. Useful for direct printing
of errors to a client or for internal development / logs.
- All created errors extend
Error
with additional methods added on. - Show errors that are safe for client / user-consumption vs internal only.
- Create your own custom error types with custom messaging, status codes and metadata.
- Errors can be created via a registry (recommended), or you can create your own error classes.
- Attach an error to your error object to get the full error chain.
- Selectively expose error metadata based on internal or external use.
- Built-in auto-completion for Typescript when searching for registered error types.
- 100% test coverage
Table of Contents
Motivation / Error handling use-cases
The basic Javascript Error
type is extremely bare bones - you can only specify a message.
In a production-level application, I've experienced the following use-cases:
- A developer should be able to add metadata to the error that may assist with troubleshooting.
- A developer should be able to reference the original error.
- Errors should be able to work with a logging framework.
- Errors should be well-formed / have a defined structure that can be consumed / emitted for analytics and services.
- Errors should be able to be cross-referenced in various systems via an identifier / error id.
- Errors should not expose sensitive data to the end-user / client.
- Errors that are exposed to the end-user / client should not reveal data that would expose system internals.
- Error responses from an API service should follow a common format.
- End-users / clients should be able to relay the error back to support; the relayed data should be enough for a developer to troubleshoot.
- Client developers prefer a list of error codes to expect from an API service so they can properly handle errors.
- You want to classify the types of errors that your application is emitting in your metrics / analytics tool.
new-error
was built with these use-cases in mind.
Installation
$ npm i new-error --save
Examples
- Define a set of high level errors
- Common high level error types could be 4xx/5xx HTTP codes
- Define a set of low level errors
- Think of low level errors as a fine-grained sub-code/category to a high level error
- Initialize the error registry with the errors
With the error registry
The error registry is the fastest way to define and create errors.
import { ErrorRegistry } from 'new-error'
const errors = {
INTERNAL_SERVER_ERROR: {
className: 'InternalServerError',
code: 'ERR_INT_500',
statusCode: 500,
logLevel: 'error',
onConvert: (err) => { return err },
message: 'Internal server error'
}
}
const errorSubCodes = {
DATABASE_FAILURE: {
message: 'There was a database failure, SQL err code %s',
subCode: 'DB_0001',
statusCode: 500,
logLevel: 'error',
onConvert: (err) => { return err }
}
}
const errRegistry = new ErrorRegistry(errors, errorSubCodes)
const err = errRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
.withErrorId('err-1234')
.formatMessage('SQL_1234')
console.log(err.toJSON())
Produces:
(You can omit fields you do not need - see usage section below.)
{
errId: 'err-1234',
name: 'InternalServerError',
code: 'ERR_INT_500',
message: 'There was a database failure, SQL err code SQL_1234',
type: 'DATABASE_FAILURE',
subCode: 'DB_0001',
statusCode: 500,
meta: {},
stack: 'InternalServerError: There was a database failure, SQL err code %s\n' +
' at ErrorRegistry.newError (new-error/src/ErrorRegistry.ts:128:12)\n' +
' at Object.<anonymous> (new-error/src/test.ts:55:25)\n' +
' at Module._compile (internal/modules/cjs/loader.js:1158:30)\n' +
' at Module._compile (new-error/node_modules/source-map-support/source-map-support.js:541:25)\n' +
' at Module.m._compile (/private/var/folders/mx/b54hc2lj3fbfsndkv4xmz8d80000gn/T/ts-node-dev-hook-20649714243977457.js:57:25)\n' +
' at Module._extensions..js (internal/modules/cjs/loader.js:1178:10)\n' +
' at require.extensions.<computed> (/private/var/folders/mx/b54hc2lj3fbfsndkv4xmz8d80000gn/T/ts-node-dev-hook-20649714243977457.js:59:14)\n' +
' at Object.nodeDevHook [as .ts] (new-error/node_modules/ts-node-dev/lib/hook.js:61:7)\n' +
' at Module.load (internal/modules/cjs/loader.js:1002:32)\n' +
' at Function.Module._load (internal/modules/cjs/loader.js:901:14)'
}
Helper utilities
Auto-generate high level error properties
generateHighLevelErrors(errorDefs, options: GenerateHighLevelErrorOpts)
If you find yourself doing the following pattern:
const errors = {
INTERNAL_SERVER_ERROR: {
className: 'InternalServerError',
code: 'INTERNAL_SERVER_ERROR',
statusCode: 500
}
}
You can use the utility method to do it instead:
import { generateHighLevelErrors } from 'new-error'
const errors = generateHighLevelErrors({
INTERNAL_SERVER_ERROR: {
statusCode: 500
}
})
- If a
className
or code
is already defined, it will not overwrite it
Configuration options
interface GenerateHighLevelErrorOpts {
disableGenerateClassName?: boolean
disableGenerateCode?: boolean
}
Auto-generate low level error properties
generateLowLevelErrors(errorDefs, options: GenerateLowLevelErrorOpts)
If you find yourself doing the following pattern:
const errors = {
DATABASE_FAILURE: {
subCode: 'DATABASE_FAILURE',
message: 'Database failure'
}
}
You can use the utility method to do it instead:
import { generateLowLevelErrors } from 'new-error'
const errors = generateLowLevelErrors({
DATABASE_FAILURE: {
message: 'Database failure'
}
})
- If a
subCode
is already defined, it will not overwrite it
Configuration options
interface GenerateLowLevelErrorOpts {
disableGenerateSubCode?: boolean
}
Class-based with low level errors without a registry
You can create concrete error classes by extending the BaseRegistryError
class, which
extends the BaseError
class.
The registry example can be also written as:
import { BaseRegistryError, LowLevelError } from 'new-error'
class InternalServerError extends BaseRegistryError {
constructor (errDef: LowLevelError) {
super({
code: 'ERR_INT_500',
statusCode: 500
}, errDef)
}
}
const err = new InternalServerError({
type: 'DATABASE_FAILURE',
message: 'There was a database failure, SQL err code %s',
subCode: 'DB_0001',
statusCode: 500,
logLevel: 'error'
})
console.log(err.formatMessage('SQL_1234').toJSON())
Bare-bones class-based error
If you want a native-style Error
, you can use BaseError
.
The registry example can be written as:
import { BaseError } from 'new-error'
class InternalServerError extends BaseError {}
const err = new InternalServerError('There was a database failure, SQL err code %s')
.withErrorType('DATABASE_FAILURE')
.withErrorCode('ERR_INT_500')
.withErrorSubCode('DB_0001')
.withStatusCode(500)
.withLogLevel('error')
console.log(err.formatMessage('SQL_1234').toJSON())
Example Express Integration
import express from 'express'
import { ErrorRegistry, BaseError } from 'new-error'
const app = express()
const port = 3000
const errors = {
INTERNAL_SERVER_ERROR: {
className: 'InternalServerError',
code: 'ERR_INT_500',
statusCode: 500
}
}
const errorSubCodes = {
DATABASE_FAILURE: {
message: 'There was a database failure.',
subCode: 'DB_0001',
statusCode: 500
}
}
const errRegistry = new ErrorRegistry(errors, errorSubCodes)
app.get('/', async (req, res, next) => {
try {
throw new Error('SQL issue')
} catch (e) {
const err = errRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
err.causedBy(err)
return next(err)
}
})
app.use((err, req, res, next) => {
if (err) {
if (err instanceof BaseError) {
err.withErrorId(Math.random().toString(36).slice(2))
console.error(JSON.stringify(err.toJSON(), null, 2))
res.status(err.getStatusCode() ?? 500)
return res.json({
err: err.toJSONSafe()
})
}
return res.json({
err: {
message: err.message
}
})
}
next()
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
If you visit http://localhost:3000
, you'll get a 500 status code, and the following response:
{"err": {"errId": "xd0v1szkziq", code":"ERR_INT_500","subCode":"DB_0001","statusCode":500,"meta":{}}}
Working with log levels
You might want to use a different log level when logging common errors, such as validation errors.
import { ErrorRegistry } from 'new-error'
const errors = {
VALIDATION_ERROR: {
className: 'ValidationError',
code: 'VALIDATION_ERROR',
statusCode: 400,
logLevel: 'debug'
}
}
const errorSubCodes = {
MISSING_FORM_FIELDS: {
message: 'Form submission data is missing fields',
subCode: 'MISSING_FORM_FIELDS',
statusCode: 400
}
}
const errRegistry = new ErrorRegistry(errors, errorSubCodes)
const err = errRegistry.newError('VALIDATION_ERROR', 'MISSING_FORM_FIELDS')
if (err.getLogLevel() === 'debug') {
console.debug(JSON.stringify(err.toJSON(), null, 2))
} else {
console.error(JSON.stringify(err.toJSON(), null, 2))
}
Error Registry API
The ErrorRegistry
is responsible for the registration and creation of errors.
Constructor
new ErrorRegistry(highLvErrors, lowLvErrors, config = {})
Configuration options
interface IErrorRegistryConfig {
baseErrorConfig?: IBaseErrorConfig
onCreateError?: (err: BaseRegistryError) => void
}
Example
const errRegistry = new ErrorRegistry(errors, errorSubCodes, {
baseErrorConfig: {
omitEmptyMetadata: true
}
})
Child registry with context
ErrorRegistry#withContext(context: IErrorRegistryContextConfig)
You can create a child registry that adds context for all new errors created. This is useful if
your body of code throws multiple errors and you want to include the same metadata for each one
without repeating yourself.
- All property references are copied to the child registry from the parent. This keeps memory usage
low as the references are re-used vs a complete clone of the data.
- Because all properties are copied over, the child registry will execute any handlers / config options
the parent has when creating new errors.
Configuration options
export interface IErrorRegistryContextConfig {
metadata?: Record<any, string>
safeMetadata?: Record<any, string>
}
Example
const childRegistry = errRegistry.withContext({
metadata: {
contextA: 'context-a'
},
safeMetadata: {
contextB: 'context-b'
}
})
const err = childRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
If we do err.toJSON()
, we should get the following output:
{
name: 'InternalServerError',
code: 'INT_ERR',
message: 'There is an issue with the database',
type: 'DATABASE_FAILURE',
subCode: 'DB_ERR',
statusCode: 500,
// err.toJSONSafe() would exclude contextA
meta: { contextA: 'context-a', contextB: 'context-b' },
stack: '...'
}
We can also append data:
const err = childRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
.withMetadata({
moreMeta: 'data'
})
If we do err.toJSON()
, we should get the following output:
{
name: 'InternalServerError',
code: 'INT_ERR',
message: 'There is an issue with the database',
type: 'DATABASE_FAILURE',
subCode: 'DB_ERR',
statusCode: 500,
// err.toJSONSafe() would exclude contextA and moreMeta
meta: { contextA: 'context-a', contextB: 'context-b', moreMeta: 'data' },
stack: '...'
}
Creating errors
Errors generated by the registry extends BaseError
.
Create a well-defined error
Method: ErrorRegistry#newError(highLevelErrorName, LowLevelErrorName)
This is the method you should generally use as you are forced to use your
well-defined high and low level error definitions. This allows for consistency
in how errors are defined and thrown.
const err = errRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
Create an error without a low-level error
Method: ErrorRegistry#newBareError(highLevelErrorName, [message])
This method does not include a low level error code, and allows direct specification of an
error message.
Specify a custom message
const err = errRegistry.newBareError('INTERNAL_SERVER_ERROR', 'An internal server error has occured.')
Use the message property from the high level error if defined
const errors = {
AUTH_REQUIRED: {
className: 'AuthRequired',
code: 'AUTH_REQ',
message: 'Auth required'
}
}
const err = errRegistry.newBareError('AUTH_REQUIRED')
Custom message not defined and high level error has no message property defined
The error will use the code as the default.
const errors = {
DB_ERROR: {
className: 'DatabaseError',
code: 'DB_ERR'
}
}
const err = errRegistry.newBareError('DB_ERROR')
Error creation handler
If you want all errors created from the registry to have defined properties, you can use the onCreateError
config option to modify the created error.
For example, if you want to create an error id for each new error:
const errRegistry = new ErrorRegistry(errors, errorSubCodes, {
onCreateError: (err) => {
err.withErrorId('test-id')
}
})
const err = errRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
instanceOf
/ comparisons
Comparing a custom error
Method: ErrorRegistry#instanceOf(classInstance, highLevelErrorName)
Performs an instanceof
operation against a custom error.
const err = errRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
if (errRegistry.instanceOf(err, 'INTERNAL_SERVER_ERROR')) {
}
Native instanceof
You can also check if the error is custom-built using this check:
import { BaseError } from 'new-error'
function handleError(err) {
if (err instanceof BaseError) {
}
}
Error API
Except for the getter and serialization methods, all other methods are chainable.
Generated errors extend the BaseError
class, which extends Error
.
Constructor
new BaseError(message: string, config: IBaseErrorConfig: IBaseErrorConfig = {})
message
: The error message you would use in new Error(message)
Configuration options
interface IBaseErrorConfig {
toJSONFieldsToOmit?: string[]
toJSONSafeFieldsToOmit?: string[]
omitEmptyMetadata?: boolean
onPreToJSONData?: (data: Partial<SerializedError>) => Partial<SerializedError>
onPreToJSONSafeData?: (data: Partial<SerializedErrorSafe>) => Partial<SerializedErrorSafe>
onConvert?: <E extends BaseError = BaseError>(err: E) => any
appendWithErrorMessageFormat?: string
}
Getters
The following getters are included with the standard Error
properties and methods:
BaseError#getErrorId()
BaseError#getRequestId()
BaseError#getErrorName()
BaseError#getCode()
BaseError#getErrorType()
BaseError#getSubCode()
BaseError#getStatusCode()
BaseError#getCausedBy()
BaseError#getMetadata()
BaseError#getSafeMetadata()
BaseError#getLogLevel()
BaseError#getConfig()
Basic setters
If you use the registry, you should not need to us these setters as the registry
sets the values already.
BaseError#withErrorType(type: string): this
BaseError#withErrorCode(code: string | number): this
BaseError#withErrorSubCode(code: string | number): this
BaseError#withLogLevel(level: string | number): this
BaseError#setConfig(config: IBaseErrorConfig): void
BaseError#setOnConvert(<E extends BaseError = BaseError>(err: E) => any): void
Static methods
static BaseError#fromJSON(data: object, options?: object): BaseError
Utility methods
BaseError#convert<E = BaseError | any>() : E
BaseError#hasOnConvertDefined(): boolean
Set an error id
Method: BaseError#withErrorId(errId: string)
Attaches an id to the error. Useful if you want to display an error id to a client / end-user
and want to cross-reference that id in an internal logging system for easier troubleshooting.
For example, you might want to use nanoid
to generate ids for errors.
import { nanoid } from 'nanoid'
err.withErrorId(nanoid())
logger.error(err.toJSON())
Set a request id
Method: BaseError#withRequestId(reqId: string)
Attaches request id to the error. Useful if you want to display the request id to a client / end-user
and want to cross-reference that id in an internal logging system for easier troubleshooting.
For example, you might want to use nanoid
to generate ids.
import { nanoid } from 'nanoid'
err.withRequestId(nanoid())
logger.error(err.toJSON())
Attaching errors
Method: BaseError#causedBy(err: any)
You can attach another error to the error.
const externalError = new Error('Some thrown error')
err.causedBy(externalError)
Append the attached error message to the main error message
If the config option appendWithErrorMessageFormat
is defined, and the error sent into causedBy
contains a message
property, then the caused by error message will be appended to the main error message.
Useful if you find yourself applying this pattern to expose the attached error message:
const thrownErrorFromApp = new Error('Duplicate key error')
const err = new BaseError('Internal server error: %s');
err.causedBy(thrownErrorFromApp)
err.formatMessage(thrownErrorFromApp.message);
This is also useful for test frameworks like jest
where it will only print out the main error message
and not any properties attached to the error.
const IS_TEST_ENV = process.env.NODE_ENV === 'test';
const err = new BaseError('Internal server error', {
appendWithErrorMessageFormat: IS_TEST_ENV ? ': %s' : null
})
err.causedBy(new Error('Duplicate key'))
console.log(err.message)
const IS_TEST_ENV = process.env.NODE_ENV === 'test';
const err = new BaseError('Internal server error: %s', {
appendWithErrorMessageFormat: IS_TEST_ENV ? '===> %s' : null
})
err.formatMessage('Hello')
err.causedBy(new Error('Duplicate key'))
console.log(err.message)
It is not recommended that appendWithErrorMessageFormat
is defined in a production environment
as the causedBy
error messages tend to be system-level messages that could be exposed to clients
if the error is being thrown back to the client.
Format messages
Method: BaseError#formatMessage(...formatParams)
See the sprintf-js
package for usage.
err.formatMessage('SQL_ERR_1234')
The message can be accessed via the .message
property.
Converting the error into another type
Method: BaseError#convert<T = any>() : T
This is useful if you need to convert the error into another type. This type can be another error or some other data type.
Apollo GraphQL example
For example, Apollo GraphQL prefers that any errors thrown from a GQL endpoint is an error that extends ApolloError
.
You might find yourself doing the following pattern if your resolver happens to throw a BaseError
:
import { GraphQLError } from 'graphql';
import { BaseError } from 'new-error';
import { ApolloError, ForbiddenError } from 'apollo-server';
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (err: GraphQLError) => {
const origError = error.originalError;
if (origError instanceof BaseError) {
switch(origError.getCode()) {
case 'PERMISSION_REQUIRED':
return new ForbiddenError(err.message)
default:
return new ApolloError(err.message)
}
}
return err;
},
});
Trying to switch for every single code / subcode can be cumbersome.
Instead of using this pattern, do the following so that your conversions can remain in one place:
import { GraphQLError } from 'graphql';
import { BaseError, ErrorRegistry } from 'new-error';
import { ApolloError, ForbiddenError } from 'apollo-server';
const errors = {
PERMISSION_REQUIRED: {
className: 'PermissionRequiredError',
code: 'PERMISSION_REQUIRED',
onConvert: (error) => {
return new ForbiddenError(error.message)
}
},
AUTH_REQUIRED: {
className: 'AuthRequiredError',
code: 'AUTH_REQUIRED'
}
}
const errorSubCodes = {
ADMIN_PANEL_RESTRICTED: {
message: 'Access scope required: admin',
onConvert: (error) => {
return new ForbiddenError('Admin required')
}
},
EDITOR_SECTION_RESTRICTED: {
message: 'Access scope required: editor',
}
}
const errRegistry = new ErrorRegistry(errors, errorSubCodes, {
onCreateError: (err) => {
if (!err.hasOnConvertDefined()) {
err.setOnConvert((err) => {
if (process.env.NODE_ENV !== 'production') {
return new ApolloError(err.message, err.getCode(), err.toJSON());
}
return new ApolloError('Internal server error', 'INTERNAL_SERVER_ERROR', {
errId: err.getErrorId(),
});
});
}
}
})
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (err: GraphQLError) => {
const origError = error.originalError;
if (origError instanceof BaseError) {
console.log(origError.toJSON())
return origError.convert()
}
console.log(err)
return err;
},
});
const resolvers = {
Query: {
adminSettings(parent, args, context, info) {
throw errRegistry.newError('PERMISSION_REQUIRED', 'ADMIN_PANEL_RESTRICTED')
},
editorSettings(parent, args, context, info) {
throw errRegistry.newError('PERMISSION_REQUIRED', 'EDITOR_SECTION_RESTRICTED')
},
checkAuth(parent, args, context, info) {
throw errRegistry.newError('AUTH_REQUIRED', 'EDITOR_SECTION_RESTRICTED')
},
checkAuth2(parent, args, context, info) {
throw errRegistry.newBareError('AUTH_REQUIRED', 'Some error message')
},
permRequired(parent, args, context, info) {
throw errRegistry.newBareError('PERMISSION_REQUIRED', 'Some error message')
}
}
}
Adding metadata
Safe metadata
Method: BaseError#withSafeMetadata(data = {})
Safe metadata would be any kind of data that you would be ok with exposing to a client, like an
HTTP response.
err.withSafeMetadata({
errorId: 'err-12345',
moreData: 1234
})
.withSafeMetadata({
requestId: 'req-12345'
})
This can also be written as:
err.withSafeMetadata({
errorId: 'err-12345',
moreData: 1234
})
err.withSafeMetadata({
requestId: 'req-12345'
})
Internal metadata
Method: BaseError#withMetadata(data = {})
Internal metadata would be any kind of data that you would not be ok with exposing to a client,
but would be useful for internal development / logging purposes.
err.withMetadata({
email: 'test@test.com'
})
.withMetadata({
userId: 'user-abcd'
})
Serializing errors
Safe serialization
Method: BaseError#toJSONSafe(fieldsToOmit = [])
Generates output that would be safe for client consumption.
- Omits
name
- Omits
message
- Omits
causedBy
- Omits
type
- Omits
logLevel
- Omits the stack trace
- Omits any data defined via
BaseError#withMetadata()
err.withSafeMetadata({
requestId: 'req-12345'
})
.toJSONSafe()
Produces:
{
code: 'ERR_INT_500',
subCode: 'DB_0001',
statusCode: 500,
meta: { requestId: 'req-12345' }
}
Internal serialization
Method: BaseError#toJSON(fieldsToOmit = [])
Generates output that would be suitable for internal use.
- Includes
name
- Includes
type
- Includes
message
- Includes
causedBy
- Includes the stack trace
- All data from
BaseError#withMetadata()
and BaseError#withSafeMetadata()
is included
err.withSafeMetadata({
reqId: 'req-12345',
}).withMetadata({
email: 'test@test.com'
})
.toJSON()
Produces:
{
name: 'InternalServerError',
code: 'ERR_INT_500',
message: 'There was a database failure, SQL err code %s',
type: 'DATABASE_FAILURE',
subCode: 'DB_0001',
statusCode: 500,
meta: { errorId: 'err-12345', requestId: 'req-12345' },
stack: 'InternalServerError: There was a database failure, SQL err code %s\n' +
' at ErrorRegistry.newError (new-error/src/ErrorRegistry.ts:128:12)\n' +
' at Object.<anonymous> (new-error/src/test.ts:55:25)\n' +
' at Module._compile (internal/modules/cjs/loader.js:1158:30)\n' +
' at Module._compile (new-error/node_modules/source-map-support/source-map-support.js:541:25)\n' +
' at Module.m._compile (/private/var/folders/mx/b54hc2lj3fbfsndkv4xmz8d80000gn/T/ts-node-dev-hook-17091160954051898.js:57:25)\n' +
' at Module._extensions..js (internal/modules/cjs/loader.js:1178:10)\n' +
' at require.extensions.<computed> (/private/var/folders/mx/b54hc2lj3fbfsndkv4xmz8d80000gn/T/ts-node-dev-hook-17091160954051898.js:59:14)\n' +
' at Object.nodeDevHook [as .ts] (new-error/node_modules/ts-node-dev/lib/hook.js:61:7)\n' +
' at Module.load (internal/modules/cjs/loader.js:1002:32)\n' +
' at Function.Module._load (internal/modules/cjs/loader.js:901:14)'
}
Post-processing handlers
The BaseError
config onPreToJSONData
/ onPreToJSONSafeData
options allow post-processing of the data. This is useful if you want to decorate your data for all new
errors created.
const errRegistry = new ErrorRegistry(errors, errorSubCodes, {
baseErrorConfig: {
onPreToJSONData: (data) => {
data.date = new Date().tostring()
if (data.meta) {
data.meta.moreData = 'test'
}
return data
}
}
})
const err = errRegistry.newError('INTERNAL_SERVER_ERROR', 'DATABASE_FAILURE')
.withErrorId('err-1234')
.formatMessage('SQL_1234')
console.log(err.toJSON())
Deserialization
Issues with deserialization
Deserialization is not perfect
- If the serialized output lacks the
name
property (not present when using toJSONSafe()
), then only a BaseError
instance can be returned. - The metadata is squashed in the serialized output that information is required to separate them.
- It is difficult to determine the original type / structure of the
causedBy
data. As a result, it will be copied as-is.
Potential security issues with deserialization
- You need to be able to trust the data you're deserializing as the serialized data can be modified in various ways by
an untrusted party.
- The deserialization implementation does not perform
JSON.parse()
as JSON.parse()
in its raw form is susceptible to
prototype pollution
if the parse function does not have a proper sanitization function. It is up to the developer to properly
trust / sanitize / parse the data.
ErrorRegistry#fromJSON()
method
This method will attempt to deserialize into a registered error type via the name
property. If it is unable to, a BaseError
instance is
returned instead.
ErrorRegistry#fromJSON(data: object, [options]: DeserializeOpts): IBaseError
data
: Data that is the output of BaseError#toJSON()
. The data must be an object, not a string.options
: Optional deserialization options.
interface DeserializeOpts {
safeMetadataFields?: {
[key: string]: true
}
}
Returns a BaseError
instance or an instance of a registered error type.
import { ErrorRegistry } from 'new-error'
const errors = {
INTERNAL_SERVER_ERROR: {
className: 'InternalServerError',
code: 'ERR_INT_500',
statusCode: 500,
logLevel: 'error'
}
}
const errorSubCodes = {
DATABASE_FAILURE: {
message: 'There was a database failure, SQL err code %s',
subCode: 'DB_0001',
statusCode: 500,
logLevel: 'error'
}
}
const errRegistry = new ErrorRegistry(errors, errorSubCodes)
const data = {
'errId': 'err-123',
'code': 'ERR_INT_500',
'subCode': 'DB_0001',
'message': 'test message',
'meta': { 'safeData': 'test454', 'test': 'test123' },
'name': 'InternalServerError',
'statusCode': 500,
'causedBy': 'test',
'stack': 'abcd'
}
const err = errRegistry.fromJSON(data, {
safeMetadataFields: {
safeData: true
}
})
static BaseError#fromJSON()
method
If you are not using the registry, you can deserialize using this method. This also applies to any class that extends
BaseError
.
static BaseError#fromJSON(data: object, [options]: DeserializeOpts): IBaseError
data
: Data that is the output of BaseError#toJSON()
. The data must be an object, not a string.options
: Optional deserialization options.
Returns a BaseError
instance or an instance of the class that extends it.
import { BaseError } from 'new-error'
const data = {
code: 'ERR_INT_500',
subCode: 'DB_0001',
statusCode: 500,
errId: 'err-1234',
meta: { requestId: 'req-12345', safeData: '123' }
}
const err = BaseError.fromJSON(data, {
safeMetadataFields: {
safeData: true
}
})
Stand-alone instance-based deserialization
If the name
property is present in the serialized data if it was serialized with toJson()
, you can use a switch
to map to an instance:
const data = {
name: 'InternalServerError',
code: 'ERR_INT_500',
subCode: 'DB_0001',
statusCode: 500,
errId: 'err-1234',
meta: { requestId: 'req-12345', safeData: '123' }
}
let err = null
switch (data.name) {
case 'InternalServerError':
return InternalServerError.fromJSON(data)
default:
return BaseError.fromJSON(data)
}
Looking for production-grade env variable / configuration management?
Check out https://github.com/theogravity/configurity