modern-errors turns error handling into a class hierarchy with a normalize function
Handle errors in a simple, stable, consistent way
At a glance
- What is it?
- modern-errors is a JavaScript library that gives you error classes, error properties, wrapping via cause, and a normalize step that turns a thrown string into a proper Error instance. It fits a codebase that wants typed error classes and instanceof checks, and it does not fit a project that only needs a good logger.
- Who is it for?
- modern-errors fits a TypeScript codebase that wants to branch on error type with instanceof, carry structured properties on errors and stop leaking raw thrown strings, and it does not fit an application whose only requirement is a stack trace in the log.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The library is a class factory, and subclassing is the whole API
modern-errors is a library for handling errors in a simple, stable, consistent way, and its central mechanism is subclassing. The entry point is a class factory, and every error type in an application is derived from it:
import ModernError from 'modern-errors'
export const BaseError = ModernError.subclass('BaseError')
export const UnknownError = BaseError.subclass('UnknownError')
export const InputError = BaseError.subclass('InputError')
export const AuthError = BaseError.subclass('AuthError')
export const DatabaseError = BaseError.subclass('DatabaseError')The result is a real JavaScript subclass. ErrorClass.subclass() returns a subclass, and parent class options are merged with subclass options, so a hierarchy of your own errors is a hierarchy of actual classes with actual inheritance.
That inheritance is what makes the checking work. The README recommends exporting and documenting all error classes so consumers can check them, which also enables sharing error classes between modules, and the check is the ordinary instanceof test:
if (error instanceof InputError) {
// ...
}The pattern of a single BaseError with the rest derived from it is what lets a caller catch a whole family at once. The example properties section shows this directly: BaseError carries props isError true, InputError adds isUserError true, and an InputError instance is an instance of both.
The dependency list explains the implementation. error-custom-class does the subclassing, error-class-utils and is-error-instance handle the class utilities and the instanceof semantics, normalize-exception does the normalising, merge-error-cause handles the cause chain, and set-error-props, set-error-message and set-error-stack are the individual setters.
Properties on an error, with a convention for hiding the internal ones
Error properties are the second feature, and there are two places to set them. Class properties are set once in the subclass options, and instance properties are passed at construction time, so a class can declare defaults that any instance can override.
throw new InputError('Invalid file path', { props: { filePath: '/...' } })The internal property convention is the part worth copying. Error properties that are internal or secret can be prefixed with a leading _ character, and that makes them non-enumerable, which prevents iterating or logging them. So userId stays enumerable and _isUserError does not, meaning a JSON.stringify of the error or a loop over its keys will not leak the flag you use for control flow.
That is a small design decision with a real payoff. If you use a property to mark an error as a user error versus a bug, that marker is exactly the sort of thing you do not want leaking into a user-facing payload, and the leading _ plus non-enumerability handles it in the object model rather than in every serialisation site.
Wrapping with cause preserves the original, normalize handles the thrown string
Two features deal with the two ways errors usually get lost. Wrapping keeps the original error as a cause, so a low-level failure is not replaced by a vague message:
try {
// ...
} catch (cause) {
throw new InputError('Could not read the file.', { cause })
}Normalisation handles the case where something that is not an Error was thrown. A library that throws a string, or a third-party dependency that rejects with a plain object, produces an error you cannot instanceof and cannot read a stack from:
try {
throw 'Missing file path.'
} catch (error) {
// Normalized from a string to a `BaseError` instance
throw BaseError.normalize(error)
}The comment in the README is explicit that the string is normalised to a BaseError instance. This is the difference between an error-handling layer that only works when everyone on the team follows the convention, and one that works at the boundary where you do not control the thrower. The dependency list confirms the mechanism, with normalize-exception and merge-error-cause doing the work.
The README also lists unknown errors as a first-class category, separating known from unknown, and the class hierarchy gives you UnknownError as a sibling of the specific ones. In practice that is what a catch-all branch keys on, and having it named rather than improvised keeps the fallback handling consistent across the codebase.
Plugins are separate packages, and the serialize one closes the loop
The plugin system is where the library extends beyond error classes. The README lists the official plugins and what each does: modern-errors-cli for handling errors in CLI modules, modern-errors-beautiful to prettify error messages and stacks, modern-errors-process for process errors, modern-errors-bugs to print where to report bugs, modern-errors-serialize to serialize and parse errors, modern-errors-clean to clean stack traces, modern-errors-http to create HTTP error responses, modern-errors-winston to log errors with Winston, and modern-errors-switch to execute class-specific logic. You can also create your own plugin, with the details in docs/plugins.md.
The serialize plugin is the one that closes the loop between server and client. The example shows a class created with the plugin attached, an error serialized as JSON, and then parsed back into an identical error instance:
export const BaseError = ModernError.subclass('BaseError', {
plugins: [modernErrorsSerialize],
})
const error = new InputError('Missing file path.')
const errorString = JSON.stringify(error)
const identicalError = BaseError.parse(JSON.parse(errorString))That matters for an API. A client that receives a serialised error can reconstruct the same class on the other side, so the same instanceof checks work in the browser and in the service, and the client does not have to know the server's class layout, only the error names. The install instructions say that if any plugin is used, it must also be installed with npm install modern-errors-{pluginName}.
The plugin is attached at subclass time, so the decision about serialization is a property of your base class rather than a per-throw decision.
The stability claims are about shape, not about fewer dependencies
The README groups the library's guarantees under stability, and they are specific. Normalize invalid errors. 100 percent test coverage, with a Codecov badge pointing at the coverage report. Strict TypeScript types, with a separate document at docs/typescript.md.
The coverage claim is the one that matters most in practice, and it is checkable by anyone: the badge links to the public Codecov page. A library whose job is to normalise the errors everyone else throws at it has a fairly specific failure surface, and coverage is a reasonable proxy for how much of it is exercised.
The strict typing claim is the other half. A library that gives you typed error classes is worthless if the types are loose, since you would end up casting at every check point. The repository has a tsconfig.json, a src/ directory with a main.d.ts, and the package exports point types at ./build/src/main.d.ts with the default at ./build/src/main.js.
The dependency count is worth naming honestly. There are ten runtime dependencies, and they are small utility packages rather than frameworks, but the list is not empty, and a project that cares about transitive dependency count should look at it. The package also sets sideEffects to false, which lets a bundler drop it entirely when nothing is imported from it, and the files field excludes test files and the helpers and fixtures directories from the published tarball.
ES modules only, Node 18.18 or newer, and one hard constraint on your build
The install is a single command:
npm install modern-errorsAnd any plugin used has to be installed too, following the same naming shape:
npm install modern-errors-{pluginName}The runtime requirement is Node.js 18.18.0 or newer, and the package also works in browsers, with a browserslist configuration referenced from the maintainer's dev-tasks repository.
The constraint worth reading twice is the module format. This is an ES module. It must be loaded using an import or import() statement, not require(). If TypeScript is used, it must be configured to output ES modules, not CommonJS.
That is a real migration consideration rather than a footnote. A CommonJS codebase that wants to use this library has to change its own module output, or use a dynamic import() at the call site, which changes how the error classes are loaded and when. The maintainer has deliberately gone the other way from the prevailing CommonJS default, and the trade is worth it for tree shaking and for the modern type export map, but it is not free.
The repository itself is a Gulp and AVA project with an ESLint config, a Prettier config, a CHANGELOG, a CONTRIBUTING file and a CODE_OF_CONDUCT file, and the test script is gulp test. The project is MIT licensed.
Editorial conclusion
modern-errors fits a TypeScript codebase that wants to branch on error type with instanceof, carry structured properties on errors and stop leaking raw thrown strings, and it does not fit an application whose only requirement is a stack trace in the log. Before adopting it, confirm your build can consume ES modules, since the package is ES module only and must be loaded with import rather than require, and decide whether the plugin set you need, such as serialization or HTTP responses, is worth installing separately.
Frequently asked questions
How do I create error classes with modern-errors?
Call ModernError.subclass('Name') to create a class, then derive further classes from it with BaseError.subclass('Name'). The result is a real JavaScript subclass, so instanceof checks work, and parent class options are merged with subclass options.
What does modern-errors normalize do?
It converts a value that is not a proper Error into an error instance of the class you call it on, so throwing a plain string inside a try block becomes a BaseError instance rather than something without a stack. The mechanism is the normalize-exception dependency.
Can I hide internal properties on a modern-errors error?
Yes. Giving a property name a leading _ makes it non-enumerable, which prevents iterating or logging it, so a userId stays enumerable while a private flag such as _isUserError stays out of JSON output.
Does modern-errors work with CommonJS?
No. The package is an ES module and must be loaded with an import or import() statement rather than require(). If you use TypeScript, it has to be configured to output ES modules instead of CommonJS. The minimum runtime is Node.js 18.18.0, and browsers are supported too.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ehmicky-modern-errors)Was this useful?