Skip to content

Commit 75b30c6

Browse files
ronagTrott
authored andcommitted
stream: emit 'error' asynchronously
errorOrDestroy emits 'error' synchronously due to compat reasons. However, it should be possible to use correct async behaviour for new code. PR-URL: #29744 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 9085c03 commit 75b30c6

13 files changed

+163
-116
lines changed

doc/api/stream.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1853,7 +1853,8 @@ methods only.
18531853
The `callback` method must be called to signal either that the write completed
18541854
successfully or failed with an error. The first argument passed to the
18551855
`callback` must be the `Error` object if the call failed or `null` if the
1856-
write succeeded.
1856+
write succeeded. The `callback` method will always be called asynchronously and
1857+
before `'error'` is emitted.
18571858

18581859
All calls to `writable.write()` that occur between the time `writable._write()`
18591860
is called and the `callback` is called will cause the written data to be

lib/_stream_writable.js

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -265,33 +265,6 @@ Writable.prototype.pipe = function() {
265265
errorOrDestroy(this, new ERR_STREAM_CANNOT_PIPE());
266266
};
267267

268-
269-
function writeAfterEnd(stream, cb) {
270-
const er = new ERR_STREAM_WRITE_AFTER_END();
271-
// TODO: defer error events consistently everywhere, not just the cb
272-
errorOrDestroy(stream, er);
273-
process.nextTick(cb, er);
274-
}
275-
276-
// Checks that a user-supplied chunk is valid, especially for the particular
277-
// mode the stream is in. Currently this means that `null` is never accepted
278-
// and undefined/non-string values are only allowed in object mode.
279-
function validChunk(stream, state, chunk, cb) {
280-
var er;
281-
282-
if (chunk === null) {
283-
er = new ERR_STREAM_NULL_VALUES();
284-
} else if (typeof chunk !== 'string' && !state.objectMode) {
285-
er = new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Buffer'], chunk);
286-
}
287-
if (er) {
288-
errorOrDestroy(stream, er);
289-
process.nextTick(cb, er);
290-
return false;
291-
}
292-
return true;
293-
}
294-
295268
Writable.prototype.write = function(chunk, encoding, cb) {
296269
const state = this._writableState;
297270
var ret = false;
@@ -315,17 +288,25 @@ Writable.prototype.write = function(chunk, encoding, cb) {
315288
if (typeof cb !== 'function')
316289
cb = nop;
317290

291+
let err;
318292
if (state.ending) {
319-
writeAfterEnd(this, cb);
293+
err = new ERR_STREAM_WRITE_AFTER_END();
320294
} else if (state.destroyed) {
321-
const err = new ERR_STREAM_DESTROYED('write');
322-
process.nextTick(cb, err);
323-
errorOrDestroy(this, err);
324-
} else if (isBuf || validChunk(this, state, chunk, cb)) {
295+
err = new ERR_STREAM_DESTROYED('write');
296+
} else if (chunk === null) {
297+
err = new ERR_STREAM_NULL_VALUES();
298+
} else if (!isBuf && typeof chunk !== 'string' && !state.objectMode) {
299+
err = new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Buffer'], chunk);
300+
} else {
325301
state.pendingcb++;
326302
ret = writeOrBuffer(this, state, chunk, encoding, cb);
327303
}
328304

305+
if (err) {
306+
process.nextTick(cb, err);
307+
errorOrDestroy(this, err, true);
308+
}
309+
329310
return ret;
330311
};
331312

@@ -629,7 +610,7 @@ Writable.prototype._write = function(chunk, encoding, cb) {
629610
if (this._writev) {
630611
this._writev([{ chunk, encoding }], cb);
631612
} else {
632-
cb(new ERR_METHOD_NOT_IMPLEMENTED('_write()'));
613+
process.nextTick(cb, new ERR_METHOD_NOT_IMPLEMENTED('_write()'));
633614
}
634615
};
635616

@@ -656,15 +637,25 @@ Writable.prototype.end = function(chunk, encoding, cb) {
656637
this.uncork();
657638
}
658639

640+
if (typeof cb !== 'function')
641+
cb = nop;
642+
659643
// Ignore unnecessary end() calls.
660-
if (!state.ending) {
644+
// TODO(ronag): Compat. Allow end() after destroy().
645+
if (!state.errored && !state.ending) {
661646
endWritable(this, state, cb);
662-
} else if (typeof cb === 'function') {
663-
if (!state.finished) {
664-
onFinished(this, state, cb);
665-
} else {
666-
cb(new ERR_STREAM_ALREADY_FINISHED('end'));
667-
}
647+
} else if (state.finished) {
648+
const err = new ERR_STREAM_ALREADY_FINISHED('end');
649+
process.nextTick(cb, err);
650+
// TODO(ronag): Compat. Don't error the stream.
651+
// errorOrDestroy(this, err, true);
652+
} else if (state.destroyed) {
653+
const err = new ERR_STREAM_DESTROYED('end');
654+
process.nextTick(cb, err);
655+
// TODO(ronag): Compat. Don't error the stream.
656+
// errorOrDestroy(this, err, true);
657+
} else if (cb !== nop) {
658+
onFinished(this, state, cb);
668659
}
669660

670661
return this;
@@ -749,7 +740,7 @@ function finish(stream, state) {
749740
function endWritable(stream, state, cb) {
750741
state.ending = true;
751742
finishMaybe(stream, state, true);
752-
if (cb) {
743+
if (cb !== nop) {
753744
if (state.finished)
754745
process.nextTick(cb);
755746
else
@@ -774,14 +765,6 @@ function onCorkedFinish(corkReq, state, err) {
774765
}
775766

776767
function onFinished(stream, state, cb) {
777-
if (state.destroyed && state.errorEmitted) {
778-
// TODO(ronag): Backwards compat. Should be moved to end() without
779-
// errorEmitted check and with errorOrDestroy.
780-
const err = new ERR_STREAM_DESTROYED('end');
781-
process.nextTick(cb, err);
782-
return;
783-
}
784-
785768
function onerror(err) {
786769
stream.removeListener('finish', onfinish);
787770
stream.removeListener('error', onerror);

lib/internal/streams/destroy.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ function undestroy() {
119119
}
120120
}
121121

122-
function errorOrDestroy(stream, err) {
122+
function errorOrDestroy(stream, err, sync) {
123123
// We have tests that rely on errors being emitted
124124
// in the same tick, so changing this is semver major.
125125
// For now when you opt-in to autoDestroy we allow
@@ -138,7 +138,12 @@ function errorOrDestroy(stream, err) {
138138
if (r) {
139139
r.errored = true;
140140
}
141-
emitErrorNT(stream, err);
141+
142+
if (sync) {
143+
process.nextTick(emitErrorNT, stream, err);
144+
} else {
145+
emitErrorNT(stream, err);
146+
}
142147
}
143148
}
144149

test/parallel/test-child-process-server-close.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const server = net.createServer((conn) => {
3232
}));
3333
}).listen(common.PIPE, () => {
3434
const client = net.connect(common.PIPE, common.mustCall());
35-
client.on('data', () => {
35+
client.once('data', () => {
3636
client.end(() => {
3737
server.close();
3838
});

test/parallel/test-file-write-stream.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
'use strict';
23-
require('../common');
23+
const common = require('../common');
2424
const assert = require('assert');
2525

2626
const path = require('path');
@@ -46,9 +46,6 @@ file
4646
callbacks.open++;
4747
assert.strictEqual(typeof fd, 'number');
4848
})
49-
.on('error', function(err) {
50-
throw err;
51-
})
5249
.on('drain', function() {
5350
console.error('drain!', callbacks.drain);
5451
callbacks.drain++;
@@ -65,17 +62,13 @@ file
6562
assert.strictEqual(file.bytesWritten, EXPECTED.length * 2);
6663

6764
callbacks.close++;
68-
assert.throws(
69-
() => {
70-
console.error('write after end should not be allowed');
71-
file.write('should not work anymore');
72-
},
73-
{
74-
code: 'ERR_STREAM_WRITE_AFTER_END',
75-
name: 'Error',
76-
message: 'write after end'
77-
}
78-
);
65+
console.error('write after end should not be allowed');
66+
file.write('should not work anymore');
67+
file.on('error', common.expectsError({
68+
code: 'ERR_STREAM_WRITE_AFTER_END',
69+
name: 'Error',
70+
message: 'write after end'
71+
}));
7972

8073
fs.unlinkSync(fn);
8174
});

test/parallel/test-net-socket-write-error.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
'use strict';
22

3-
require('../common');
4-
const assert = require('assert');
3+
const common = require('../common');
54
const net = require('net');
65

76
const server = net.createServer().listen(0, connectToServer);
87

98
function connectToServer() {
109
const client = net.createConnection(this.address().port, () => {
11-
assert.throws(() => client.write(1337),
12-
{
13-
code: 'ERR_INVALID_ARG_TYPE',
14-
name: 'TypeError'
15-
});
10+
client.write(1337, common.expectsError({
11+
code: 'ERR_INVALID_ARG_TYPE',
12+
name: 'TypeError'
13+
}));
14+
client.on('error', common.expectsError({
15+
code: 'ERR_INVALID_ARG_TYPE',
16+
name: 'TypeError'
17+
}));
1618

1719
client.destroy();
1820
})

test/parallel/test-net-write-arguments.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
'use strict';
22
const common = require('../common');
3-
const assert = require('assert');
43
const net = require('net');
54

65
const socket = net.Stream({ highWaterMark: 0 });
76

87
// Make sure that anything besides a buffer or a string throws.
9-
assert.throws(() => socket.write(null),
10-
{
11-
code: 'ERR_STREAM_NULL_VALUES',
12-
name: 'TypeError',
13-
message: 'May not write null values to stream'
14-
});
8+
socket.write(null, common.expectsError({
9+
code: 'ERR_STREAM_NULL_VALUES',
10+
name: 'TypeError',
11+
message: 'May not write null values to stream'
12+
}));
13+
socket.on('error', common.expectsError({
14+
code: 'ERR_STREAM_NULL_VALUES',
15+
name: 'TypeError',
16+
message: 'May not write null values to stream'
17+
}));
18+
1519
[
1620
true,
1721
false,

test/parallel/test-stream-writable-destroy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ const assert = require('assert');
341341
write.destroy();
342342
let ticked = false;
343343
write.end(common.mustCall((err) => {
344-
assert.strictEqual(ticked, false);
344+
assert.strictEqual(ticked, true);
345345
assert.strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED');
346346
}));
347347
ticked = true;

test/parallel/test-stream-writable-end-multiple.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ const assert = require('assert');
66
const stream = require('stream');
77

88
const writable = new stream.Writable();
9-
109
writable._write = (chunk, encoding, cb) => {
1110
setTimeout(() => cb(), 10);
1211
};
1312

1413
writable.end('testing ended state', common.mustCall());
1514
writable.end(common.mustCall());
1615
writable.on('finish', common.mustCall(() => {
16+
let ticked = false;
1717
writable.end(common.mustCall((err) => {
18+
assert.strictEqual(ticked, true);
1819
assert.strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED');
1920
}));
21+
ticked = true;
2022
}));

test/parallel/test-stream-writable-null.js

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
require('../common');
2+
const common = require('../common');
33
const assert = require('assert');
44

55
const stream = require('stream');
@@ -14,33 +14,29 @@ class MyWritable extends stream.Writable {
1414
}
1515
}
1616

17-
assert.throws(
18-
() => {
19-
const m = new MyWritable({ objectMode: true });
20-
m.write(null, (err) => assert.ok(err));
21-
},
22-
{
17+
{
18+
const m = new MyWritable({ objectMode: true });
19+
m.write(null, (err) => assert.ok(err));
20+
m.on('error', common.expectsError({
2321
code: 'ERR_STREAM_NULL_VALUES',
2422
name: 'TypeError',
2523
message: 'May not write null values to stream'
26-
}
27-
);
24+
}));
25+
}
2826

2927
{ // Should not throw.
3028
const m = new MyWritable({ objectMode: true }).on('error', assert);
3129
m.write(null, assert);
3230
}
3331

34-
assert.throws(
35-
() => {
36-
const m = new MyWritable();
37-
m.write(false, (err) => assert.ok(err));
38-
},
39-
{
32+
{
33+
const m = new MyWritable();
34+
m.write(false, (err) => assert.ok(err));
35+
m.on('error', common.expectsError({
4036
code: 'ERR_INVALID_ARG_TYPE',
4137
name: 'TypeError'
42-
}
43-
);
38+
}));
39+
}
4440

4541
{ // Should not throw.
4642
const m = new MyWritable().on('error', assert);

0 commit comments

Comments
 (0)