Gestione degli errori con i flussi node.js


164

Qual è il modo corretto di gestire gli errori con i flussi? So già che c'è un evento di "errore" che puoi ascoltare, ma voglio sapere qualche dettaglio in più su situazioni arbitrariamente complicate.

Per cominciare, cosa fai quando vuoi fare una semplice catena di tubi:

input.pipe(transformA).pipe(transformB).pipe(transformC)...

E come si crea correttamente una di quelle trasformazioni in modo che gli errori vengano gestiti correttamente?

Domande più correlate:

  • quando si verifica un errore, cosa succede all'evento 'end'? Non viene mai licenziato? A volte viene licenziato? Dipende dalla trasformazione / flusso? Quali sono gli standard qui?
  • ci sono meccanismi per propagare errori attraverso i tubi?
  • i domini risolvono efficacemente questo problema? Gli esempi sarebbero belli.
  • gli errori che derivano da eventi "errore" hanno tracce di stack? Qualche volta? Mai? c'è un modo per ottenerne uno da loro?

1
Questo non è banale. Promisei framework rendono tutto molto più semplice
slezica,

27
Purtroppo le promesse / i futures non possono davvero aiutarti con gli stream ...
BT,

Risposte:


222

trasformare

I flussi di trasformazione sono sia leggibili che scrivibili, e quindi sono flussi "medi" davvero buoni. Per questo motivo, a volte vengono definiti throughflussi. Sono simili a un flusso duplex in questo modo, tranne per il fatto che forniscono un'interfaccia piacevole per manipolare i dati anziché semplicemente inviarli. Lo scopo di un flusso di trasformazione è manipolare i dati mentre vengono inviati attraverso il flusso. Potresti voler fare alcune chiamate asincrone, ad esempio, o derivare un paio di campi, rimappare alcune cose, ecc.


Dove potresti mettere un flusso di trasformazione


Per come creare un flusso di trasformazione, vedere qui e qui . Tutto quello che devi fare è:

  1. include il modulo stream
  2. istanziare (o ereditare da) la classe Transform
  3. implementare un _transformmetodo che richiede a (chunk, encoding, callback).

Il pezzo è i tuoi dati. La maggior parte delle volte non dovrai preoccuparti della codifica se lavori objectMode = true. Il callback viene chiamato al termine dell'elaborazione del blocco. Questo blocco viene quindi trasferito al flusso successivo.

Se vuoi un bel modulo di supporto che ti permetta di fare lo streaming davvero molto facilmente, ti suggerisco through2 .

Per la gestione degli errori, continua a leggere.

tubo

In una catena di condotte, la gestione degli errori non è affatto banale. Secondo questo thread .pipe () non è costruito per inoltrare errori. Quindi qualcosa come ...

var a = createStream();
a.pipe(b).pipe(c).on('error', function(e){handleError(e)});

... ascolterebbe solo errori sullo stream c. Se si generasse un evento di errore a, questo non verrebbe trasmesso e, di fatto, verrebbe lanciato. Per farlo correttamente:

var a = createStream();
a.on('error', function(e){handleError(e)})
.pipe(b)
.on('error', function(e){handleError(e)})
.pipe(c)
.on('error', function(e){handleError(e)});

Ora, sebbene il secondo modo sia più dettagliato, puoi almeno mantenere il contesto in cui si verificano i tuoi errori. Questa è generalmente una buona cosa.

Una libreria che trovo utile, tuttavia, se hai un caso in cui vuoi solo catturare gli errori a destinazione e non ti interessa tanto dove è successo è il flusso di eventi .

fine

Quando viene generato un evento di errore, l'evento finale non verrà generato (esplicitamente). L'emissione di un evento di errore interromperà il flusso.

domini

Nella mia esperienza, i domini funzionano davvero bene per la maggior parte del tempo. Se si verifica un evento di errore non gestito (ovvero l'emissione di un errore su uno stream senza un listener), il server può arrestarsi in modo anomalo. Ora, come sottolineato nell'articolo precedente, puoi avvolgere lo stream in un dominio che dovrebbe rilevare correttamente tutti gli errori.

var d = domain.create();
 d.on('error', handleAllErrors);
 d.run(function() {
     fs.createReadStream(tarball)
       .pipe(gzip.Gunzip())
       .pipe(tar.Extract({ path: targetPath }))
       .on('close', cb);
 });

Il bello dei domini è che manterranno le tracce dello stack. Anche il flusso di eventi fa un buon lavoro anche in questo.

Per ulteriori letture, consulta il manuale del flusso . Abbastanza approfondito, ma super utile e offre ottimi collegamenti a molti moduli utili.


Sono davvero informazioni fantastiche, grazie! Potresti aggiungere un po 'del perché vuoi creare un flusso di trasformazione e perché si collega alla mia domanda?
BT

Certo, anche se l'ho immaginato correlato da quando me l'hai chiesto; )
mshell_lauren

1
Pubblica questo post su isaccs su Google Gruppi- nodejs: groups.google.com/d/msg/nodejs/lJYT9hZxFu0/L59CFbqWGyYJ (non grokbase)
jpillora

Questa risposta è scritta perfettamente. Esaminerò il suggerimento sul dominio: sembra essere il tipo di soluzione che stavo cercando.
Punto

12
Si noti che non è necessario avvolgere il .on('error')gestore in una funzione anonima, ad esempio a.on('error', function(e){handleError(e)})può esserea.on('error', handleError)
timoxley

28

Se si utilizza node> = v10.0.0 è possibile utilizzare stream.pipeline e stream.finished .

Per esempio:

const { pipeline, finished } = require('stream');

pipeline(
  input, 
  transformA, 
  transformB, 
  transformC, 
  (err) => {
    if (err) {
      console.error('Pipeline failed', err);
    } else {
      console.log('Pipeline succeeded');
    }
});


finished(input, (err) => {
  if (err) {
    console.error('Stream failed', err);
  } else {
    console.log('Stream is done reading');
  }
});

Vedi questo PR github per ulteriori discussioni.


1
Perché dovresti usare finished, tuttavia, quando pipelineha già un callback?
Marcos Pereira,

4
È possibile che si desideri gestire gli errori in modo diverso tra la pipeline e i singoli flussi.
Shusson,

25

i domini sono obsoleti. non hai bisogno di loro.

per questa domanda, le distinzioni tra trasformabile o scrivibile non sono così importanti.

La risposta di mshell_lauren è ottima, ma in alternativa puoi anche ascoltare esplicitamente l'evento di errore su ogni flusso che ritieni possa essere un errore. e riutilizzare la funzione gestore se si preferisce.

var a = createReadableStream()
var b = anotherTypeOfStream()
var c = createWriteStream()

a.on('error', handler)
b.on('error', handler)
c.on('error', handler)

a.pipe(b).pipe(c)

function handler (err) { console.log(err) }

così facendo si evita la famigerata eccezione non rilevata nel caso in cui uno di questi flussi generasse il suo evento di errore


3
lol si diverte a gestire 3 diversi eventi di errore e prega affinché chiunque abbia scritto le 3 diverse librerie di streaming abbia implementato correttamente la gestione degli errori
Alexander Mills

4
@Alex Mills 1) Qual è il problema della gestione di 3 eventi e perché sono "diversi", quando il loro tipo è lo stesso - errorsi può anche accontentarsi del fatto che ogni evento è distinto; 2) quali librerie di streaming sono scritte sopra, oltre alla funzionalità nativa Node.js? e 3) perché è importante il modo in cui gestiscono gli eventi internamente, quando questo ovviamente consente a chiunque di collegare gestori di errori aggiuntivi a tutto ciò che è già presente?
AMN

10

Gli errori dell'intera catena possono essere propagati allo stream più a destra usando una semplice funzione:

function safePipe (readable, transforms) {
    while (transforms.length > 0) {
        var new_readable = transforms.shift();
        readable.on("error", function(e) { new_readable.emit("error", e); });
        readable.pipe(new_readable);
        readable = new_readable;
    }
    return readable;
}

che può essere usato come:

safePipe(readable, [ transform1, transform2, ... ]);

5

.on("error", handler)si occupa solo degli errori di streaming, ma se si utilizzano flussi Transform personalizzati, .on("error", handler)non intercettare gli errori che si verificano all'interno_transform funzione. Quindi si può fare qualcosa del genere per controllare il flusso dell'applicazione: -

thisla parola chiave in _transformfunzione si riferisce a Streamse stessa, che è un EventEmitter. Quindi puoi usare try catchcome sotto per catturare gli errori e in seguito passarli ai gestori di eventi personalizzati.

// CustomTransform.js
CustomTransformStream.prototype._transform = function (data, enc, done) {
  var stream = this
  try {
    // Do your transform code
  } catch (e) {
    // Now based on the error type, with an if or switch statement
    stream.emit("CTError1", e)
    stream.emit("CTError2", e)
  }
  done()
}

// StreamImplementation.js
someReadStream
  .pipe(CustomTransformStream)
  .on("CTError1", function (e) { console.log(e) })
  .on("CTError2", function (e) { /*Lets do something else*/ })
  .pipe(someWriteStream)

In questo modo, è possibile mantenere separati i gestori di logica ed errori. Inoltre, puoi scegliere di gestire solo alcuni errori e ignorarne altri.

UPDATE
Alternativa: RXJS Osservabile


4

Utilizzare il pacchetto multipipe per combinare più flussi in un flusso duplex. E gestire gli errori in un unico posto.

const pipe = require('multipipe')

// pipe streams
const stream = pipe(streamA, streamB, streamC) 


// centralized error handling
stream.on('error', fn)

1

Utilizzare il modello Node.js creando una meccanica del flusso Transform e chiamando il callback donecon un argomento per propagare l'errore:

var transformStream1 = new stream.Transform(/*{objectMode: true}*/);

transformStream1.prototype._transform = function (chunk, encoding, done) {
  //var stream = this;

  try {
    // Do your transform code
    /* ... */
  } catch (error) {
    // nodejs style for propagating an error
    return done(error);
  }

  // Here, everything went well
  done();
}

// Let's use the transform stream, assuming `someReadStream`
// and `someWriteStream` have been defined before
someReadStream
  .pipe(transformStream1)
  .on('error', function (error) {
    console.error('Error in transformStream1:');
    console.error(error);
    process.exit(-1);
   })
  .pipe(someWriteStream)
  .on('close', function () {
    console.log('OK.');
    process.exit();
  })
  .on('error', function (error) {
    console.error(error);
    process.exit(-1);
   });

Hmm, quindi stai dicendo che se tutti i processori di stream fossero costruiti in questo modo, gli errori si propagerebbero?
BT,

-2

Try catch non acquisirà gli errori che si sono verificati nello stream perché vengono generati dopo la chiusura del codice chiamante. puoi fare riferimento alla documentazione:

https://nodejs.org/dist/latest-v10.x/docs/api/errors.html


Grazie, ma questo non risponde affatto alla domanda.
BT,

Darmi un documento di 40 pagine non è utile. A cosa pensi che dovrei fare riferimento in quella pagina gigante? Inoltre, hai letto la mia domanda? La mia domanda non è "prova a catturare il lavoro con i flussi?" Sono già ben consapevole del fatto che try-catch non funzionerà con errori asincroni, ad esempio quelli delle pipeline di elaborazione dei flussi.
BT
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.