Skip to content

Commit a6acfdd

Browse files
author
Ruben Bridgewater
committed
Improve error handling
Added individual error classes Don't silently fail for commands without callback from now on General polishing (e.g. better error messages)
1 parent bf39492 commit a6acfdd

18 files changed

+363
-127
lines changed

README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ are passed an object containing `delay` (in ms) and `attempt` (the attempt #) at
137137
### "error"
138138

139139
`client` will emit `error` when encountering an error connecting to the Redis server or when any other in node_redis occurs.
140+
If you use a command without callback and encounter a ReplyError it is going to be emitted to the error listener.
140141

141142
So please attach the error listener to node_redis.
142143

@@ -293,6 +294,50 @@ client.get("foo_rand000000000000", function (err, reply) {
293294

294295
`client.end()` without the flush parameter set to true should NOT be used in production!
295296

297+
## Error handling (>= v.2.6)
298+
299+
All redis errors are returned as `ReplyError`.
300+
All unresolved commands that get rejected due to what ever reason return a `AbortError`.
301+
As subclass of the `AbortError` a `AggregateError` exists. This is emitted in case multiple unresolved commands without callback got rejected in debug_mode.
302+
They are all aggregated and a single error is emitted in that case.
303+
304+
Example:
305+
```js
306+
var redis = require('./');
307+
var assert = require('assert');
308+
var client = redis.createClient();
309+
310+
client.on('error', function (err) {
311+
assert(err instanceof Error);
312+
assert(err instanceof redis.AbortError);
313+
assert(err instanceof redis.AggregateError);
314+
assert.strictEqual(err.errors.length, 2); // The set and get got aggregated in here
315+
assert.strictEqual(err.code, 'NR_CLOSED');
316+
});
317+
client.set('foo', 123, 'bar', function (err, res) { // To many arguments
318+
console.log(err, res);
319+
assert(err instanceof redis.ReplyError); // => true
320+
assert.strictEqual(err.command, 'SET');
321+
assert.deepStrictEqual(err.args, ['foo', 123, 'bar']);
322+
323+
client.set('foo', 'bar');
324+
client.get('foo');
325+
process.nextTick(function () {
326+
client.end(true); // Force closing the connection while the command did not yet return
327+
});
328+
});
329+
330+
```
331+
332+
Every `ReplyError` contains the `command` name in all-caps and the arguments (`args`).
333+
334+
If node_redis emits a library error because of another error, the triggering error is added to the returned error as `origin` attribute.
335+
336+
___Error codes___
337+
338+
node_redis returns a `NR_CLOSED` error code if the clients connection dropped. If a command unresolved command got rejected a `UNERCTAIN_STATE` code is returned.
339+
A `CONNECTION_BROKEN` error code is used in case node_redis gives up to reconnect.
340+
296341
## client.unref()
297342

298343
Call `unref()` on the underlying socket connection to the Redis server, allowing the program to exit once no more commands are pending.
@@ -537,7 +582,7 @@ Redis. The interface in `node_redis` is to return an individual `Batch` object b
537582
The only difference between .batch and .multi is that no transaction is going to be used.
538583
Be aware that the errors are - just like in multi statements - in the result. Otherwise both, errors and results could be returned at the same time.
539584

540-
If you fire many commands at once this is going to **boost the execution speed by up to 400%** [sic!] compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired.
585+
If you fire many commands at once this is going to boost the execution speed significantly compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired.
541586

542587
## Monitor mode
543588

index.js

Lines changed: 103 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var tls = require('tls');
55
var util = require('util');
66
var utils = require('./lib/utils');
77
var Queue = require('double-ended-queue');
8-
var CommandError = require('./lib/customError');
8+
var errorClasses = require('./lib/customErrors');
99
var Command = require('./lib/command').Command;
1010
var OfflineCommand = require('./lib/command').OfflineCommand;
1111
var EventEmitter = require('events');
@@ -189,13 +189,24 @@ function create_parser (self) {
189189
self.return_reply(data);
190190
},
191191
returnError: function (err) {
192-
self.return_error(err);
192+
// Return a ReplyError to indicate Redis returned an error
193+
self.return_error(new errorClasses.ReplyError(err));
193194
},
194195
returnFatalError: function (err) {
195196
// Error out all fired commands. Otherwise they might rely on faulty data. We have to reconnect to get in a working state again
196-
self.flush_and_error(err, ['command_queue']);
197-
self.stream.destroy();
198-
self.return_error(err);
197+
// Note: the execution order is important. First flush and emit, then create the stream
198+
err = new errorClasses.ReplyError(err);
199+
err.message += '. Please report this.';
200+
self.ready = false;
201+
self.flush_and_error({
202+
message: 'Fatal error encountert. Command aborted.',
203+
code: 'NR_FATAL'
204+
}, {
205+
error: err,
206+
queues: ['command_queue']
207+
});
208+
self.emit('error', err);
209+
self.create_stream();
199210
},
200211
returnBuffers: self.buffers || self.message_buffers,
201212
name: self.options.parser,
@@ -240,7 +251,7 @@ RedisClient.prototype.create_stream = function () {
240251
this.stream.setTimeout(this.connect_timeout, function () {
241252
// Note: This is only tested if a internet connection is established
242253
self.retry_totaltime = self.connect_timeout;
243-
self.connection_gone('timeout', new Error('Redis connection gone from timeout event'));
254+
self.connection_gone('timeout');
244255
});
245256
}
246257

@@ -270,11 +281,11 @@ RedisClient.prototype.create_stream = function () {
270281
});
271282

272283
this.stream.once('close', function (hadError) {
273-
self.connection_gone('close', hadError ? new Error('Stream connection closed with a transmission error') : null);
284+
self.connection_gone('close');
274285
});
275286

276287
this.stream.once('end', function () {
277-
self.connection_gone('end', null);
288+
self.connection_gone('end');
278289
});
279290

280291
this.stream.on('drain', function () {
@@ -325,30 +336,46 @@ RedisClient.prototype.warn = function (msg) {
325336
};
326337

327338
// Flush provided queues, erroring any items with a callback first
328-
RedisClient.prototype.flush_and_error = function (error, queue_names) {
329-
var callbacks_not_called = [];
330-
queue_names = queue_names || ['offline_queue', 'command_queue'];
339+
RedisClient.prototype.flush_and_error = function (error_attributes, options) {
340+
options = options || {};
341+
var aggregated_errors = [];
342+
var queue_names = options.queues || ['command_queue', 'offline_queue']; // Flush the command_queue first to keep the order intakt
331343
for (var i = 0; i < queue_names.length; i++) {
344+
// If the command was fired it might have been processed so far
345+
if (queue_names[i] === 'command_queue') {
346+
error_attributes.message += ' It might have been processed.';
347+
} else { // As the command_queue is flushed first, remove this for the offline queue
348+
error_attributes.message = error_attributes.message.replace(' It might have been processed.', '');
349+
}
350+
// Don't flush everything from the queue
332351
for (var command_obj = this[queue_names[i]].shift(); command_obj; command_obj = this[queue_names[i]].shift()) {
333-
var err = new CommandError(error);
352+
var err = new errorClasses.AbortError(error_attributes);
334353
err.command = command_obj.command.toUpperCase();
335-
if (command_obj.args.length) {
354+
if (command_obj.args && command_obj.args.length) {
336355
err.args = command_obj.args;
337356
}
357+
if (options.error) {
358+
err.origin = options.error;
359+
}
338360
if (typeof command_obj.callback === 'function') {
339361
command_obj.callback(err);
340362
} else {
341-
callbacks_not_called.push(err);
363+
aggregated_errors.push(err);
342364
}
343365
}
344-
this[queue_names[i]] = new Queue();
345366
}
346-
// Mutate the original error that will be emitted
347-
// This is fine, as we don't manipulate any user errors
348-
if (callbacks_not_called.length !== 0) {
349-
error.errors = callbacks_not_called;
367+
// Currently this would be a breaking change, therefore it's only emitted in debug_mode
368+
if (exports.debug_mode && aggregated_errors.length) {
369+
var error;
370+
if (aggregated_errors.length === 1) {
371+
error = aggregated_errors[0];
372+
} else {
373+
error_attributes.message = error_attributes.message.replace('It', 'They').replace(/command/i, '$&s');
374+
error = new errorClasses.AggregateError(error_attributes);
375+
error.errors = aggregated_errors;
376+
}
377+
this.emit('error', error);
350378
}
351-
return callbacks_not_called.length === 0;
352379
};
353380

354381
RedisClient.prototype.on_error = function (err) {
@@ -538,6 +565,7 @@ RedisClient.prototype.connection_gone = function (why, error) {
538565
if (this.retry_timer) {
539566
return;
540567
}
568+
error = error || null;
541569

542570
debug('Redis connection is gone from ' + why + ' event.');
543571
this.connected = false;
@@ -564,9 +592,12 @@ RedisClient.prototype.connection_gone = function (why, error) {
564592
// If this is a requested shutdown, then don't retry
565593
if (this.closing) {
566594
debug('Connection ended by quit / end command, not retrying.');
567-
error = new Error('Stream connection ended and running command aborted. It might have been processed.');
568-
error.code = 'NR_OFFLINE';
569-
this.flush_and_error(error);
595+
this.flush_and_error({
596+
message: 'Stream connection ended and command aborted.',
597+
code: 'NR_CLOSED'
598+
}, {
599+
error: error
600+
});
570601
return;
571602
}
572603

@@ -586,31 +617,39 @@ RedisClient.prototype.connection_gone = function (why, error) {
586617
if (typeof this.retry_delay !== 'number') {
587618
// Pass individual error through
588619
if (this.retry_delay instanceof Error) {
589-
error = new CommandError(this.retry_delay);
590-
}
591-
// Attention: there might be the case where there's no error!
592-
if (!error) {
593-
error = new Error('Stream connection ended and running command aborted. It might have been processed.');
594-
error.code = 'NR_OFFLINE';
595-
}
596-
// Only emit an error in case that a running command had no callback
597-
if (!this.flush_and_error(error)) {
598-
error.message = 'Stream connection ended and all running commands aborted. They might have been processed.';
599-
this.emit('error', error);
620+
error = this.retry_delay;
600621
}
622+
this.flush_and_error({
623+
message: 'Stream connection ended and command aborted.',
624+
code: 'NR_CLOSED'
625+
}, {
626+
error: error
627+
});
601628
this.end(false);
602629
return;
603630
}
604631
}
605632

606633
if (this.max_attempts !== 0 && this.attempts >= this.max_attempts || this.retry_totaltime >= this.connect_timeout) {
607-
var message = this.retry_totaltime >= this.connect_timeout ?
608-
'connection timeout exceeded.' :
609-
'maximum connection attempts exceeded.';
610-
error = new Error('Redis connection in broken state: ' + message);
611-
error.code = 'CONNECTION_BROKEN';
612-
this.flush_and_error(error);
613-
this.emit('error', error);
634+
var message = 'Redis connection in broken state: ';
635+
if (this.retry_totaltime >= this.connect_timeout) {
636+
message += 'connection timeout exceeded.';
637+
} else {
638+
message += 'maximum connection attempts exceeded.';
639+
}
640+
641+
this.flush_and_error({
642+
message: message,
643+
code: 'CONNECTION_BROKEN',
644+
}, {
645+
error: error
646+
});
647+
var err = new Error(message);
648+
err.code = 'CONNECTION_BROKEN';
649+
if (error) {
650+
err.origin = error;
651+
}
652+
this.emit('error', err);
614653
this.end(false);
615654
return;
616655
}
@@ -620,13 +659,13 @@ RedisClient.prototype.connection_gone = function (why, error) {
620659
this.offline_queue.unshift.apply(this.offline_queue, this.command_queue.toArray());
621660
this.command_queue.clear();
622661
} else if (this.command_queue.length !== 0) {
623-
error = new Error('Redis connection lost and command aborted in uncertain state. It might have been processed.');
624-
error.code = 'UNCERTAIN_STATE';
625-
if (!this.flush_and_error(error, ['command_queue'])) {
626-
// Only emit if not all commands had a callback that already handled the error
627-
error.message = 'Redis connection lost and commands aborted in uncertain state. They might have been processed.';
628-
this.emit('error', error);
629-
}
662+
this.flush_and_error({
663+
message: 'Redis connection lost and command aborted.',
664+
code: 'UNCERTAIN_STATE'
665+
}, {
666+
error: error,
667+
queues: ['command_queue']
668+
});
630669
}
631670

632671
if (this.retry_max_delay !== null && this.retry_delay > this.retry_max_delay) {
@@ -643,11 +682,9 @@ RedisClient.prototype.connection_gone = function (why, error) {
643682

644683
RedisClient.prototype.return_error = function (err) {
645684
var command_obj = this.command_queue.shift();
646-
if (command_obj && command_obj.command && command_obj.command.toUpperCase) {
647-
err.command = command_obj.command.toUpperCase();
648-
if (command_obj.args.length) {
649-
err.args = command_obj.args;
650-
}
685+
err.command = command_obj.command.toUpperCase();
686+
if (command_obj.args && command_obj.args.length) {
687+
err.args = command_obj.args;
651688
}
652689

653690
// Count down pub sub mode if in entering modus
@@ -661,7 +698,7 @@ RedisClient.prototype.return_error = function (err) {
661698
err.code = match[1];
662699
}
663700

664-
utils.callback_or_emit(this, command_obj && command_obj.callback, err);
701+
utils.callback_or_emit(this, command_obj.callback, err);
665702
};
666703

667704
RedisClient.prototype.drain = function () {
@@ -809,14 +846,16 @@ function handle_offline_command (self, command_obj) {
809846
msg = 'Stream not writeable.';
810847
}
811848
} else {
812-
msg = 'The connection has already been closed.';
849+
msg = 'The connection is already closed.';
813850
}
814-
err = new Error(command + " can't be processed. " + msg);
815-
err.command = command;
816-
if (command_obj.args.length) {
851+
err = new errorClasses.AbortError({
852+
message: command + " can't be processed. " + msg,
853+
code: 'NR_CLOSED',
854+
command: command
855+
});
856+
if (command_obj.args && command_obj.args.length) {
817857
err.args = command_obj.args;
818858
}
819-
err.code = 'NR_OFFLINE';
820859
utils.reply_in_order(self, callback, err);
821860
} else {
822861
debug('Queueing ' + command + ' for next server connection.');
@@ -889,8 +928,8 @@ RedisClient.prototype.internal_send_command = function (command, args, callback,
889928
args_copy[i] = '' + args[i];
890929
}
891930
}
892-
args = null;
893-
command_obj = new Command(command, args_copy, buffer_args, callback);
931+
// Pass the original args to make sure in error cases the original arguments are returned
932+
command_obj = new Command(command, args, buffer_args, callback);
894933

895934
if (this.options.prefix) {
896935
prefix_keys = commands.getKeyIndexes(command, args_copy);
@@ -1053,6 +1092,9 @@ exports.createClient = function () {
10531092
exports.RedisClient = RedisClient;
10541093
exports.print = utils.print;
10551094
exports.Multi = require('./lib/multi');
1095+
exports.AbortError = errorClasses.AbortError;
1096+
exports.ReplyError = errorClasses.ReplyError;
1097+
exports.AggregateError = errorClasses.AggregateError;
10561098

10571099
// Add all redis commands / node_redis api to the client
10581100
require('./lib/individualCommands');

lib/customError.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)