Skip to content

Commit 5fca27b

Browse files
committed
lib: add chainErrors API to avoid error swallowing
1 parent 3b2863d commit 5fca27b

File tree

10 files changed

+102
-22
lines changed

10 files changed

+102
-22
lines changed

lib/_http_client.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const { Buffer } = require('buffer');
5959
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
6060
const { URL, urlToHttpOptions, searchParamsSymbol } = require('internal/url');
6161
const { kOutHeaders, kNeedDrain } = require('internal/http');
62-
const { connResetException, codes } = require('internal/errors');
62+
const { chainErrors, connResetException, codes } = require('internal/errors');
6363
const {
6464
ERR_HTTP_HEADERS_SENT,
6565
ERR_INVALID_ARG_TYPE,
@@ -789,14 +789,14 @@ function onSocketNT(req, socket, err) {
789789
if (!err && req.agent && !socket.destroyed) {
790790
socket.emit('free');
791791
} else {
792-
finished(socket.destroy(err || req[kError]), (er) => {
793-
_destroy(req, er || err);
792+
finished(socket.destroy(chainErrors(req[kError], err)), (er) => {
793+
_destroy(req, chainErrors(err, er));
794794
});
795795
return;
796796
}
797797
}
798798

799-
_destroy(req, err || req[kError]);
799+
_destroy(req, chainErrors(req[kError], err));
800800
} else {
801801
tickOnSocket(req, socket);
802802
req._flush();

lib/fs.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const { isArrayBufferView } = require('internal/util/types');
7474
const binding = internalBinding('fs');
7575
const { Buffer } = require('buffer');
7676
const {
77+
chainErrors,
7778
codes: {
7879
ERR_FS_FILE_TOO_LARGE,
7980
ERR_INVALID_ARG_VALUE,
@@ -826,7 +827,7 @@ function truncate(path, len, callback) {
826827
const req = new FSReqCallback();
827828
req.oncomplete = function oncomplete(er) {
828829
fs.close(fd, (er2) => {
829-
callback(er || er2);
830+
callback(chainErrors(er2, er));
830831
});
831832
};
832833
binding.ftruncate(fd, len, req);
@@ -1294,7 +1295,7 @@ function lchmod(path, mode, callback) {
12941295
// but still try to close, and report closing errors if they occur.
12951296
fs.fchmod(fd, mode, (err) => {
12961297
fs.close(fd, (err2) => {
1297-
callback(err || err2);
1298+
callback(chainErrors(err2, err));
12981299
});
12991300
});
13001301
});
@@ -1462,8 +1463,11 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
14621463
if (isUserFd) {
14631464
callback(lazyDOMException('The operation was aborted', 'AbortError'));
14641465
} else {
1465-
fs.close(fd, function() {
1466-
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1466+
fs.close(fd, (err) => {
1467+
callback(chainErrors(
1468+
err,
1469+
lazyDOMException('The operation was aborted', 'AbortError')
1470+
));
14671471
});
14681472
}
14691473
return;
@@ -1474,8 +1478,8 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
14741478
if (isUserFd) {
14751479
callback(writeErr);
14761480
} else {
1477-
fs.close(fd, function close() {
1478-
callback(writeErr);
1481+
fs.close(fd, (err) => {
1482+
callback(chainErrors(err, writeErr));
14791483
});
14801484
}
14811485
} else if (written === length) {

lib/internal/errors.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ const prepareStackTrace = (globalThis, error, trace) => {
111111
// Error: Message
112112
// at function (file)
113113
// at file
114+
// cause: Error: Message
115+
// at function (file)
116+
// at file
114117
const errorString = ErrorPrototypeToString(error);
115118
if (trace.length === 0) {
116119
return errorString;
@@ -136,6 +139,14 @@ const maybeOverridePrepareStackTrace = (globalThis, error, trace) => {
136139
return kNoOverride;
137140
};
138141

142+
const chainErrors = (error, originalError) => {
143+
if (error) {
144+
error.cause = originalError;
145+
return error;
146+
}
147+
return originalError;
148+
};
149+
139150
// Lazily loaded
140151
let util;
141152
let assert;
@@ -752,6 +763,7 @@ class AbortError extends Error {
752763
}
753764
module.exports = {
754765
addCodeToName, // Exported for NghttpError
766+
chainErrors,
755767
codes,
756768
dnsException,
757769
errnoException,

lib/internal/fs/read_file_context.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { Buffer } = require('buffer');
1010

1111
const { FSReqCallback, close, read } = internalBinding('fs');
1212

13-
const { hideStackFrames } = require('internal/errors');
13+
const { chainErrors, hideStackFrames } = require('internal/errors');
1414

1515

1616
let DOMException;
@@ -56,7 +56,7 @@ function readFileAfterClose(err) {
5656
let buffer = null;
5757

5858
if (context.err || err)
59-
return callback(context.err || err);
59+
return callback(chainErrors(err, context.err));
6060

6161
try {
6262
if (context.size === 0)

lib/internal/fs/streams.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ const {
1212
} = primordials;
1313

1414
const {
15-
ERR_INVALID_ARG_TYPE,
16-
ERR_OUT_OF_RANGE,
17-
ERR_METHOD_NOT_IMPLEMENTED,
18-
} = require('internal/errors').codes;
15+
chainErrors,
16+
codes: {
17+
ERR_INVALID_ARG_TYPE,
18+
ERR_OUT_OF_RANGE,
19+
ERR_METHOD_NOT_IMPLEMENTED,
20+
}
21+
} = require('internal/errors');
1922
const { deprecate } = require('internal/util');
2023
const {
2124
validateFunction,
@@ -276,7 +279,7 @@ ReadStream.prototype._destroy = function(err, cb) {
276279
// to close while used in a pending read or write operation. Wait for
277280
// any pending IO (kIsPerformingIO) to complete (kIoDone).
278281
if (this[kIsPerformingIO]) {
279-
this.once(kIoDone, (er) => close(this, err || er, cb));
282+
this.once(kIoDone, (er) => close(this, chainErrors(er, err), cb));
280283
} else {
281284
close(this, err, cb);
282285
}
@@ -437,7 +440,7 @@ WriteStream.prototype._destroy = function(err, cb) {
437440
// to close while used in a pending read or write operation. Wait for
438441
// any pending IO (kIsPerformingIO) to complete (kIoDone).
439442
if (this[kIsPerformingIO]) {
440-
this.once(kIoDone, (er) => close(this, err || er, cb));
443+
this.once(kIoDone, (er) => close(this, chainErrors(er, err), cb));
441444
} else {
442445
close(this, err, cb);
443446
}

lib/internal/http2/core.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const {
6565
},
6666
} = require('internal/async_hooks');
6767
const {
68+
chainErrors,
6869
codes: {
6970
ERR_HTTP2_ALTSVC_INVALID_ORIGIN,
7071
ERR_HTTP2_ALTSVC_LENGTH,
@@ -2077,7 +2078,7 @@ class Http2Stream extends Duplex {
20772078
let endCheckCallbackErr;
20782079
const done = () => {
20792080
if (waitingForEndCheck || waitingForWriteCallback) return;
2080-
const err = writeCallbackErr || endCheckCallbackErr;
2081+
const err = chainErrors(endCheckCallbackErr, writeCallbackErr);
20812082
// writeGeneric does not destroy on error and
20822083
// we cannot enable autoDestroy,
20832084
// so make sure to destroy on error.

lib/internal/streams/destroy.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
'use strict';
22

33
const {
4-
ERR_MULTIPLE_CALLBACK
5-
} = require('internal/errors').codes;
4+
chainErrors,
5+
codes: {
6+
ERR_MULTIPLE_CALLBACK,
7+
},
8+
} = require('internal/errors');
69
const {
710
FunctionPrototypeCall,
811
Symbol,
@@ -56,7 +59,7 @@ function destroy(err, cb) {
5659
// If still constructing then defer calling _destroy.
5760
if (!s.constructed) {
5861
this.once(kDestroy, function(er) {
59-
_destroy(this, err || er, cb);
62+
_destroy(this, chainErrors(er, err), cb);
6063
});
6164
} else {
6265
_destroy(this, err, cb);

test/message/error_chainErrors.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
require('../common');
5+
const { chainErrors } = require('internal/errors');
6+
7+
const originalError = new Error('original');
8+
const err = new Error('second error');
9+
10+
throw chainErrors(err, originalError);

test/message/error_chainErrors.out

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
*/error_chainErrors.js:*
2+
throw chainErrors(err, originalError);
3+
^
4+
Error: second error
5+
at Object.<anonymous> (*test*message*error_chainErrors.js:*:*)
6+
at Module._compile (node:internal/modules/cjs/loader:*:*)
7+
at Object.Module._extensions..js (node:internal/modules/cjs/loader:*:*)
8+
at Module.load (node:internal/modules/cjs/loader:*:*)
9+
at Function.Module._load (node:internal/modules/cjs/loader:*:*)
10+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:*:*)
11+
at node:internal/main/run_main_module:*:* {
12+
cause: Error: original
13+
at Object.<anonymous> (*test*message*error_chainErrors.js:*:*)
14+
at Module._compile (node:internal/modules/cjs/loader:*:*)
15+
at Object.Module._extensions..js (node:internal/modules/cjs/loader:*:*)
16+
at Module.load (node:internal/modules/cjs/loader:*:*)
17+
at Function.Module._load (node:internal/modules/cjs/loader:*:*)
18+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:*:*)
19+
at node:internal/main/run_main_module:*:*
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
const { chainErrors } = require('internal/errors');
7+
8+
assert.strictEqual(chainErrors(null, null), null);
9+
10+
{
11+
const err = new Error();
12+
assert.strictEqual(chainErrors(null, err), err);
13+
}
14+
15+
{
16+
const err = new Error();
17+
assert.strictEqual(chainErrors(err, null), err);
18+
}
19+
20+
{
21+
const originalError = new Error('original');
22+
const err = new Error('second error');
23+
24+
const chainedError = chainErrors(err, originalError);
25+
assert.strictEqual(chainedError, err);
26+
assert.strictEqual(chainedError.cause, originalError);
27+
}

0 commit comments

Comments
 (0)