vlts Error.cause convention
npm install @vltpkg/error-causeUtility functions for Error creation to help enforce vlt'sError.cause conventions.
Usage ·
Error Reporting ·
Conventions · Error Types
Most node programs have a mishmash of error codes and various Error
subtypes, all in different shapes, making error handling and reporting
more difficult at the top level. This negatively impacts debugging and
user experience.
The JavaScript Error constructor has acause option
which is supported since Node 16.9. We should use it!
This module makes that easy.
``js
import { error, typeError } from '@vltpkg/error-cause'
// create an error when a lower-level thing fails
try {
doSomethign()
} catch (er) {
throw error('The something for the whatever failed', er)
}
// create an error with some extra information
if (!thing.valid) {
throw error('the thing is not valid', {
code: 'EINVAL',
found: thing,
})
}
// create an error from a lower-level error with extra info
try {
doSomethign(thing)
} catch (er) {
throw error('the thing is not valid', {
code: 'EINVAL',
found: thing,
cause: er,
})
}
// create an error and prune some stack frames
// use this when we want to report the location of a
// function call, not its internals.
const checkBar = () => {
if (!bar) {
// will report from the checkBar() call, not here.
throw error('no bar', { found: bar, wanted: true }, checkBar)
}
// ...
}
`
The functions will create an error object with a cause property ifcause
set, and the type checks will ensure that the object matches
vlt's conventions.
- Provide enough information to be useful. On full inspection, we
should ideally always get back to not just the initial error that
was thrown, but also all locations where the error might have been
caught and handled in some way.
- Do not provide more information than is useful. Eg,
console.error(er) should not fill the entire scrollback buffer.
- New modules and libraries should have minimal friction in creating a
new style of error when needed. This means, minimize the amount that
any module needs to know about the errors raised by any other
module, including especially top-level error handling.
- _Some_ information about the error must be known to our top-level
error handler, so that it can usefully report errors and suggest
corrections.
- A strictly upheld convention of Error object creation using the
cause property.
- Top level error handler can have special logic where necessary for
known error codes, but will still be able to do something more
useful when an Error object follows our conventions, even if it's
not a code that it knows.
The following conventions should be followed for all Error creation
and handling throughout the vlt codebase.
- If you can't help, get out of the way. Just let throws pass
through to the top when nothing can be done to assist.
- Add information by using thrown error as cause. Use a
previously-thrown error as the cause option.cause
- Add even more info with a double-. If more info can because
added to a prior throw, nest the properties like{ some, other, info, cause: priorError }
.cause
- Always set , even if no prior error. Use a plain-oldError
JavaScript object following our field conventions.
- Rare exception: synthetic ErrnoException style errors. If we are
doing something that is similar to a system operation, it's
sometimes ok to mimic node's pattern.
- Do not subclass . Just create a plain old Error, and setcause
the with additional information.
Whenever possible, if no remediation or extra information can usefully
be added, it's best to just not handle errors and let them be raised
at the higher level. For example, instead of this:
``
let data
try {
data = await readFile(someFile)
} catch (er) {
throw new Error('could not read some file!')
}
this is preferred:
`js`
const data = await readFile(someFile)
If we can add information or do something else useful for the user in
understanding the problem, do so by creating a new Error and settingcause
the original thrown error as the .
`js`
let data
try {
data = await readFile(someFile, 'utf8')
} catch (er) {
// adds semantic information about what the file was for
throw error('The lock file was not found', er)
}
If we can add even more information, that should ideally _not_ be put
on the Error we throw, but on a cause object. Because cause
objects can nest, we can do something like this:
`jscould not resolve '${name}'
let data
try {
data = await readFile(someFile, 'utf8')
} catch (er) {
throw error(, {`
// extra data about the situation
// it's ok to put big noisy objects in here, not on the error
// object itself!
name,
spec,
target,
// original error that was thrown
cause: er,
})
}
Instead of this:
``
throw Object.assign(new Error('could not resolve'), {
code: 'ERESOLVE',
from,
spec,
registry,
})
Do this instead:
`js`
throw error('could not resolve', {
code: 'ERESOLVE',
from,
spec,
registry,
})
This makes any big objects easily skipped if we want to just output
the error with console.error() or something, but still preserves any
debugging information that might be useful all the way down the chain.
In some rare low-level cases, there are operations we perform that are
very similar to a node filesystem operation.
For example, the @vltpkg/which module raises an error that isENOENT
intentionally similar to node's filesystem errors, because
that is semantically sensible.
In those cases, the error _must_ follow node's conventions as close as
possible. If we feel the need to add additional information beyond a
known system error code, string path, etc., or if the message isn't
one that is typically raised by the underlying system, then it's a
good sign that we ought to be creating an Error with a cause so
that it can be reported more usefully.
In such cases, this is fine:
`js`
// identical to the error thrown by node's fs
throw Object.assign(new Error('not found'), {
path: someFile,
code: 'ENOENT',
})
But this is way out of bounds and makes no sense:
``
throw Object.assign(new Error('could not resolve'), {
code: 'EPERM',
spec,
config: someHugeConfigObjectOrSomething,
})
Do not copy properties from a lower-level error or cause onto the
new cause object. That is unnecessary, and obscures the origin of
problems. Instead, just include the lower-level error as the cause
property. If you already have a low-level error, you don't need to
invent a synthetic one!
For example, do not do this:
``
let data
try {
data = await readFile(lockFile, 'utf8')
} catch (er) {
throw error('lockfile not found', {
code: er.code,
path: er.path,
})
}
Instead, do this:
`js`
let data
try {
data = await readFile(lockFile, 'utf8')
} catch (er) {
throw new Error('lockfile not found', { cause: er })
}
Just use the Error classes defined in the language. Additionalcause
information about error causes should be on the property, not
implicit in the constructor type.
I.e. do not do this:
``
class VersionError extends Error {
version?: Version
constructor(version: Version | string) {
super('Could not version')
this.version = Version.parse(version)
}
}
// ...
throw new VersionError(myVersion)
Instead, do this:
`js`
throw error('Could not version', { version })
Field ConventionsAll of these are optional. Additional fields may be used where
appropriate, and should be added to this list over time.
- cause - The cause field within a cause object should always beError
an object that was previously thrown. Note that the causename
on an Error itself might _also_ be a previously thrown error, if no
additional information could be usefully added beyond improving the
message.
- - String. The name of something.offset
- - Number. The offset in a Buffer or file where we areregistry
trying to read or write.
- - String or URL. A package registry.code
- - This must be a string if set, and should only be present ifERESOLVE
it's one of our creation, not a code raised on a system error. Eg,
, not ENOENT.path
- - The target of a file system operation.target
- - path on disk that is being written or extracted tospec
- - a @vltpkg/spec.Spec object relevant to the operation thatfrom
failed.
- - string. The file path origin of a resolution that failed,file:
for example in the case of relative specifiers.status
- - Number or null. Either the exit code of a process or ansignal
HTTP response status code.
- - NodeJS.Signals string or null, indicating the signalvalidOptions
that terminated a process.
- - Array of valid options when something is not adid you mean X?
valid option. (For use in output.)todo
- - String message indicating what bit of work this might be a{ todo: 'nested workspace support' }
part of, what feature needs to be implemented, etc. Eg,
.wanted
- - A desired value that was not found, or a regularfound
expression or other pattern describing it.
- - The actual value, which was not wanted.max
- - A maximum value, which was exceeded.min
- - A minimum value, which was not met.response
- - An HTTP response or@vltpkg/registry-client.CacheEntry
url
- - A string or URL objectrepository
- - String git repository remoteversion
- - string or @vltpkg/semver.Versionrange
- - string or @vltpkg/semver.Rangemanifest
- - @vltpkg/pick-manifest.Manifestpackument
- - @vltpkg/pick-manifest.Packumentcwd
- - The current working directory of a process that failed
- If there is a _type_ problem with an argument, for example a
string was expected and a number was provided, throw aTypeError
. Do not use it for a value that is the correct typestring
but otherwise invalid, such as a argument that is actuallystring
a but does not match an expected pattern.SyntaxError
- If the type is fine, but a parsed string is invalid and not
parseable, use .Error`.
- In all other cases, use