Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
defekt is custom errors made simple.
Category | Status |
---|---|
Version | |
Dependencies | |
Dev dependencies | |
Build | |
License |
$ npm install defekt
defekt
To create custom errors, create new classes and let them extend the anonymous class created by defekt
:
import { defekt } from 'defekt';
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
The code
identifies the error and can be used to differ between various types of errors.
You may set a defaultMessage
that is used when displaying the error. If you don't set a defaultMessage
, a human-readable version of the code
is used:
import { defekt } from 'defekt';
class TokenMalformed extends defekt({
code: 'TokenMalformed',
defaultMessage: 'The token is malformed.'
}) {}
class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
const tokenMalformed = new TokenMalformed();
const tokenExpired = new TokenExpired();
console.log(tokenMalformed.message);
// => 'The token is malformed.'
console.log(tokenExpired.message);
// => 'Token expired.'
These custom errors can be used in various ways. They are, however, preferred to be passed around as objects, preferably wrapped in a Result
type, instead of being thrown. This allows the handling of recoverable errors in a type-safe way, instead of using unchecked and unpredictable thrown exceptions or rejections.
import { defekt, Result } from 'defekt';
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
const validateToken = function (token: string): Result<DecodedToken, TokenMalformed | TokenExpired> {
// ...
};
const tokenResult = validateToken(rawToken);
if (tokenResult.hasError()) {
const { error } = tokenResult;
switch (error.code) {
// TypeScript will support you here and only allow the codes of the two possible errors.
case TokenMalformed.code: {
// ...
}
case TokenExpired.code: {
// ...
}
}
}
The custom errors created by this package take several parameters. They provide a default message, but you can override it:
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
const error = new TokenMalformed('Token is not valid JSON.');
You can instead provide an object, which can contain an optional cause for the error or additional data:
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
try {
// ...
} catch (ex: unknown) {
const error = new TokenMalformed({ cause: ex });
// ...
}
const error = new TokenMalformed({ data: { foo: 'bar' }});
Sometimes you need to serialize and deserialize your errors. Afterwards they are missing their prototype-chain and Error
-related functionality. To restore those, you can hydrate a raw object to a CustomError
-instance:
import { defekt, hydrateCustomError } from 'defekt';
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
const serializedTokenMalformedError = JSON.stringify(new TokenMalformed());
const rawEx = JSON.parse(serializedTokenMalformedError);
const ex = hydrateCustomError({ rawEx, potentialErrorConstructors: [ TokenMalformed ] }).unwrapOrThrow();
Note that the hydrated error is wrapped in a Result
. If the raw error can not be hydrated using one of the given potential error constructors, an error-Result
will be returned, which tells you, why the hydration was unsuccessful.
Also note that the cause
of a CustomError
is currently not hydrated, but left as-is.
Usually, JavaScript Error
s are not well suited for JSON-serialization. To improve this, the CustomError
class implements toJSON()
, which defines custom JSON-serialization behavior. If you want to be able to serialize your cause
and data
as well, they need to be either plain objects or also implement the toJSON
method.
Custom errors can be type-guarded using isCustomError
. With only one parameter it specifies an error's type to CustomError
:
try {
// ...
} catch (ex: unknown) {
if (isCustomError(error)) {
// In this scope error is of type CustomError.
}
// ...
}
You can supply the specific custom error constructor you want to check for as the second parameter:
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
try {
// ...
} catch (ex: unknown) {
if (isCustomError(error, TokenMalformed)) {
// In this scope error is of type CustomError<'TokenMalformed'>.
// This is usually functionally equivalent to the type TokenMalformed,
// but has slight differences if e.g. you define properties on the
// TokenMalformed class.
}
// ...
}
Result
Error handling is an integral part of reliable applications. Unfortunately, TypeScript does not provide a way to type-check exceptions or to even annotate functions with information about the exceptions they might throw. This makes all exceptions in TypeScript unchecked, unpredictable, and unreliable.
In addition to that, JavaScript - and, by extension, TypeScript - does not differentiate between recoverable errors and unrecoverable errors. We recommend the blog post The Error Model by Joe Duffy on the differences between the two kinds of errors and various ways to implement them.
This library aims to differentiate between recoverable errors and unrecoverable ones, by wrapping recoverable errors in a data structure. This approach is a more fancy version of the basic concept of error codes. Wrapping errors in data structures that have semantics is an attempt to bring concepts from languages like Haskell into TypeScript. Consider this situation:
const configuration = await loadConfiguration();
await startServer(configuration.port ?? 3000);
Here, loadConfiguration
might fail for several reasons. It might try to access the files system and fail because the configuration file does not exist. It might also fail because the configuration file is too large and the process runs out of memory. The former you want to handle since there is a default value for the port. The latter you don't want to handle, since you can't do anything about it. So imagine loadConfiguration
would announce its recoverable errors in its signature:
import fs from 'fs';
import { error, value } from 'defekt';
const loadConfiguration = async function (): Promise<Result<Configuration, ConfigurationNotFound>> {
try {
return value(
JSON.parse(
await fs.promises.readFile(configFilePath, 'utf8')
)
);
} catch (ex) {
if (ex.code === 'ENOENT') {
return error(new ConfigurationNotFound({
message: 'Failed to read configuration file.',
cause: ex
}));
}
throw ex;
}
};
const configuration = (await loadConfiguration()).unwrapOrDefault({ port: 3000 });
await startServer(configuration.port);
Here, any errors related to the configuration file missing are caught, propagated, and handled explicitly. If JSON.parse
fails or if the process runs out of memory, an exception will be thrown and not be handled.
There are two ways to construct a Result
. A result can either be a ResultValue
or a ResultError
:
import { error, value, Result } from 'defekt';
const errorResult: Result<unknown, Error> = error(new Error());
const valueResult: Result<number, unknown> = value(5);
// Both are assignable to a Result with matching type parameters.
let result = Result<number, Error>;
result = valueResult;
result = errorResult;
When you get a result from a function, you can check whether it has failed and act appropriately:
import { Result } from 'defekt';
const someResult: Result<number, Error> = calculateStuff();
if (someResult.hasError()) {
// Propagate the error so that callers may handle it.
return someResult;
}
console.log(someResult.value);
Alternatively you can use hasValue
to achieve the opposite.
There is a more convenient solution, if you don't need to propagate your errors:
import { Result } from 'defekt';
class TokenMalformed extends defekt({ code: 'TokenMalformed' }) {}
class TokenExpired extends defekt({ code: 'TokenExpired' }) {}
const validateToken = function (token: string): Result<DecodedToken, TokenMalformed | TokenExpired> {
// ...
};
const token = validateToken('a token').unwrapOrDefault({ sub: 'anonymous' });
// Or, if you can't handle the possible errors appropriately and
// instead want to throw the error, possibly crashing your application:
const token = validateToken('a token').unwrapOrThrow();
// If you want to transform the error to add additional information, or to
// fulfill a more general error type, you can also pass a callback:
const token = validateToken('a token').unwrapOrThrow(
err => new BroaderError({ message: 'Something went wrong', cause: err })
);
// If you want to handle errors by returning a conditional default
// value, you can use `unwrapOrElse` to supply a handler:
const token = validateToken('a token').unwrapOrElse(
(ex) => {
switch (ex.code) {
case TokenMalformed.code: {
return { sub: 'anonymous', reason: 'malformed' };
}
case TokenExpired.code: {
return { sub: 'anonymous', reason: 'expired' };
}
}
}
);
Result
If you need to assert the type of a Result
, you can use the isResult
type-guard:
import { isResult } from 'defekt';
const someValue: any = someFunction();
if (isResult(someValue)) {
// In this scope someValue is of type Result<any, any>.
}
Result
Like for errors, there is a function to hydrate a Result
from raw data in case you need to serialize and deserialize a Result
.
import { defekt, hydrateResult } from 'defekt';
const rawResult = JSON.parse(resultFromSomewhere);
const hydrationResult = hydrateResult({ rawResult });
if (hydrationResult.hasError()) {
// The hydration has failed.
} else {
const result = hydrationResult.value;
if (result.hasError()) {
// Continue with your normal error handling.
}
}
You can also optionally let hydrateResult
hydrate the contained error by passing potentialErrorConstructors
. This works identically to hydrateResult
.
isError
The function isError
is used to recognize anything that is derived from the built-in Error
class. It relies solely on the prototype chain. Use it for example in a catch
clause when trying to determine, wether what you have caught is actually an error:
import { isError } from 'defekt';
try {
// ...
} catch (ex: unknown) {
if (isError(ex)) {
// You can now access ex.message, ex.stack, ...
}
}
isCustomError
In addition to recognizing things that are derived from Error
, isCustomError
recognizes things that are derived from CustomError
and even lets you identify specific error types.
You can either identify a general CustomError
:
import { isCustomError } from 'defekt';
try {
// ...
} catch (ex: unknown) {
if (isCustomError(ex)) {
// You can now access ex.message, ex.stack, ..., but also ex.code.
}
}
Or you can pass a CustomError
constructor to make sure you have a specific type of error in hand:
import { defekt, isCustomError } from 'defekt';
class MyCustomError extends defekt({ code: 'MyCustomError' }) {}
try {
// ...
} catch (ex: unknown) {
if (isCustomError(ex, MyCustomError)) {
// In this block ex is of type `MyCustomError`.
}
}
ensureUnknownIsError
One of the greatest regrets of JavaScript is the ability to throw anything. If you want to bullet-proof your error handling, you need to check that what you catch in a catch
clause is actually an Error
. ensureUnknownIsError
takes something you caught and wraps it in an Error
if necessary. If the caught thing already is an Error
, ensureUnknownIsError
returns it unchanged.
import {ensureUnknownIsError} from "./ensureUnknownIsError";
try {
// ...
} catch (ex: unknown) {
const error = ensureUnknownIsError({ error: ex });
// Now you can go on with your usual error handling and rest assured, that
// `error` is actually an `Error`.
}
To run quality assurance for this module use roboter:
$ npx roboter
FAQs
defekt is custom errors made simple.
The npm package defekt receives a total of 27,990 weekly downloads. As such, defekt popularity was classified as popular.
We found that defekt demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 5 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.