try / catch blocks com async / await

116

Estou cavando no recurso assíncrono / aguardar do nó 7 e continuo encontrando códigos como este

function getQuote() {
  let quote = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
  return quote;
}

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

main();

Esta parece ser a única possibilidade de resolver / rejeitar ou retornar / lançar com async / await, no entanto, a v8 não otimiza o código dentro dos blocos try / catch ?!

Existem alternativas?

Patrick
fonte
O que significa 'lançar após espera não foi bem-sucedido'? Se houver erros? Se não retorna o resultado esperado? Você poderia relançar no bloco de captura.
DevDig
afaik v8 otimiza try / catch, uma instrução de lançamento é lenta
Tamas Hegedus
1
Ainda não entendo a pergunta. Você vai usar o encadeamento da velha promessa, mas não acho que seria mais rápido. Então você está preocupado com o desempenho do try-catch? Então o que fazer com async await?
Tamas Hegedus
Verifique minha resposta. Tentei obter uma abordagem mais limpa
zardilior
Aqui você pode fazer isto stackoverflow.com/a/61833084/6482248 Parece mais limpo
Prathamesh More

Respostas:

133

Alternativas

Uma alternativa para isso:

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

seria algo assim, usando promessas explicitamente:

function main() {
  getQuote().then((quote) => {
    console.log(quote);
  }).catch((error) => {
    console.error(error);
  });
}

ou algo assim, usando o estilo de passagem de continuação:

function main() {
  getQuote((error, quote) => {
    if (error) {
      console.error(error);
    } else {
      console.log(quote);
    }
  });
}

Exemplo original

O que o seu código original faz é suspender a execução e esperar que a promessa retornada por getQuote()se cumpra. Em seguida, ele continua a execução e grava o valor retornado emvar quote e o imprime se a promessa foi resolvida, ou lança uma exceção e executa o bloco catch que imprime o erro se a promessa foi rejeitada.

Você pode fazer a mesma coisa usando a API Promise diretamente, como no segundo exemplo.

atuação

Agora, para a performance. Vamos testar!

Acabei de escrever este código - f1()1como um valor de retorno, f2()lança 1como uma exceção:

function f1() {
  return 1;
}

function f2() {
  throw 1;
}

Agora vamos chamar o mesmo código milhões de vezes, primeiro com f1():

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f1();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

E então vamos mudar f1()para f2():

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f2();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

Este é o resultado que obtive para f1:

$ time node throw-test.js 
1000000

real    0m0.073s
user    0m0.070s
sys     0m0.004s

Isso é o que eu tenho para f2:

$ time node throw-test.js 
1000000

real    0m0.632s
user    0m0.629s
sys     0m0.004s

Parece que você pode fazer algo como 2 milhões de jogadas por segundo em um processo de thread único. Se você estiver fazendo mais do que isso, talvez precise se preocupar com isso.

Resumo

Eu não me preocuparia com coisas assim em Node. Se coisas assim forem muito usadas, então serão otimizadas eventualmente pelas equipes V8 ou SpiderMonkey ou Chakra e todos irão seguir - não é como se não fosse otimizado como um princípio, simplesmente não é um problema.

Mesmo se não estiver otimizado, eu ainda diria que se você está maximizando sua CPU no Node, então você provavelmente deve escrever seu cálculo de número em C - é para isso que servem os addons nativos, entre outras coisas. Ou talvez coisas como node.native sejam mais adequadas para o trabalho do que Node.js.

Estou me perguntando qual seria um caso de uso que precisa lançar tantas exceções. Normalmente, lançar uma exceção em vez de retornar um valor é, bem, uma exceção.

rsp
fonte
Eu sei que o código pode ser facilmente escrito com Promises, como mencionei, já vi isso em vários exemplos, é por isso que estou perguntando. Ter uma única operação em try / catch pode não ser um problema, mas várias funções assíncronas / aguardar com lógica de aplicativo adicional podem ser.
Patrick
4
@Patrick "pode ​​ser" e "será" é a diferença entre especulação e teste real. Eu testei para uma única instrução porque era isso que estava em sua pergunta, mas você pode facilmente converter meus exemplos para testar várias instruções. Também forneci várias outras opções para escrever código assíncrono, sobre as quais você também perguntou. Se responder à sua pergunta, você pode aceitar a resposta . Resumindo: é claro que as exceções são mais lentas do que os retornos, mas seu uso deve ser uma exceção.
rsp
1
Lançar uma exceção é, de fato, considerado uma exceção. Dito isso, o código não é otimizado, independentemente de você lançar uma exceção ou não. O impacto no desempenho vem do uso try catch, não de lançar uma exceção. Embora os números sejam pequenos, é quase 10 vezes mais lento de acordo com seus testes, o que não é insignificante.
Nepoxx
21

Alternativa semelhante ao tratamento de erros em Golang

Como async / await usa promessas por baixo do capô, você pode escrever uma pequena função de utilitário como esta:

export function catchEm(promise) {
  return promise.then(data => [null, data])
    .catch(err => [err]);
}

Em seguida, importe-o sempre que precisar detectar alguns erros e envolva sua função assíncrona, que retorna uma promessa com ela.

import catchEm from 'utility';

async performAsyncWork() {
  const [err, data] = await catchEm(asyncFunction(arg1, arg2));
  if (err) {
    // handle errors
  } else {
    // use data
  }
}
Steve Banton
fonte
Eu criei um pacote NPM que faz exatamente o acima - npmjs.com/package/@simmo/task
Mike
2
@Mike Você pode estar reinventando a roda - já existe um pacote popular que faz exatamente isso: npmjs.com/package/await-to-js
Jakub Kukul
21

Uma alternativa para o bloco try-catch é await-to-js lib. Costumo usá-lo. Por exemplo:

import to from 'await-to-js';

async function main(callback) {
    const [err,quote] = await to(getQuote());
    
    if(err || !quote) return callback(new Error('No Quote found'));

    callback(null,quote);

}

Essa sintaxe é muito mais limpa quando comparada ao try-catch.

Pulkit chadha
fonte
Tentei isso e adorei. Código limpo e legível à custa da instalação de um novo módulo. Mas se você está planejando escrever muitas funções assíncronas, devo dizer que é uma ótima adição! Obrigado
filipbarak
15
async function main() {
  var getQuoteError
  var quote = await getQuote().catch(err => { getQuoteError = err }

  if (getQuoteError) return console.error(err)

  console.log(quote)
}

Alternativamente, em vez de declarar uma possível var para conter um erro no topo, você pode fazer

if (quote instanceof Error) {
  // ...
}

Embora isso não funcione se algo como um erro TypeError ou Reference for lançado. Você pode garantir que é um erro regular, embora com

async function main() {
  var quote = await getQuote().catch(err => {
    console.error(err)      

    return new Error('Error getting quote')
  })

  if (quote instanceOf Error) return quote // get out of here or do whatever

  console.log(quote)
}

Minha preferência por isso é envolver tudo em um grande bloco try-catch, onde há várias promessas sendo criadas, o que pode tornar complicado lidar com o erro especificamente para a promessa que o criou. Com a alternativa de vários blocos try-catch, que considero igualmente complicados

Tony
fonte
8

Uma alternativa mais limpa seria a seguinte:

Devido ao fato de que toda função assíncrona é tecnicamente uma promessa

Você pode adicionar capturas às funções ao chamá-las com await

async function a(){
    let error;

    // log the error on the parent
    await b().catch((err)=>console.log('b.failed'))

    // change an error variable
    await c().catch((err)=>{error=true; console.log(err)})

    // return whatever you want
    return error ? d() : null;
}
a().catch(()=>console.log('main program failed'))

Não há necessidade de try catch, já que todos os erros de promessas são tratados e você não tem erros de código, você pode omitir isso no pai !!

Digamos que você esteja trabalhando com mongodb; se houver um erro, você pode preferir manipulá-lo na função que o chama do que criando wrappers ou usando try catches.

Zardilior
fonte
Você tem 3 funções. Um obtendo valores e detectando o erro, outro você retorna se não houver erro e finalmente uma chamada para a primeira função com um callback para verificar se aquele retornou um erro. Tudo isso é resolvido por uma única "promessa" .then (cb) .catch (cb) ou bloco trycatch.
Chefe Koshi
@Chiefkoshi Como você pode ver, um único problema não funcionaria, já que o erro está sendo tratado de maneira diferente nos três casos. Se o primeiro falhar, ele retorna d (), se o segundo falhar, ele retorna nulo se o último falhar, uma mensagem de erro diferente é exibida. A questão pede para lidar com erros ao usar await. Então essa é a resposta também. Todos devem ser executados se algum falhar. Try catch blocks exigiria três deles neste exemplo específico, que não é mais limpo
zardilior
1
A questão não pede para ser executada após promessas falhadas. Aqui você espera por B, executa C e retorna D se houver erro. Como isso é mais limpo? C tem que esperar por B, mas eles são independentes um do outro. Não vejo uma razão pela qual eles estariam em A juntos se fossem independentes. Se eles dependessem um do outro, você desejaria interromper a execução de C se B falhar, o trabalho de .then.catch ou try-catch. Presumo que eles não retornem nada e realizem algumas ações assíncronas completamente não relacionadas a A. Por que são chamados com async await?
Chefe Koshi
A questão é sobre alternativas para tentar blocos catch para lidar com erros ao usar async / await. O exemplo aqui deve ser descritivo e nada mais que um exemplo. Ele mostra o manuseio individual de operações independentes de forma sequencial, que normalmente é como async / await são usados. Por que eles são chamados com async await, é apenas para mostrar como isso pode ser tratado. É descritivo mais do que justificado.
zardilior
2

Eu gostaria de fazer assim :)

const sthError = () => Promise.reject('sth error');

const test = opts => {
  return (async () => {

    // do sth
    await sthError();
    return 'ok';

  })().catch(err => {
    console.error(err); // error will be catched there 
  });
};

test().then(ret => {
  console.log(ret);
});

É semelhante a lidar com erros com co

const test = opts => {
  return co(function*() {

    // do sth
    yield sthError();
    return 'ok';

  }).catch(err => {
    console.error(err);
  });
};
Cooper Hsiung
fonte
O código não é muito claro, cara, mas parece interessante, você poderia editar?
zardilior
É uma pena que não haja nenhuma explicação nesta resposta porque ela realmente demonstra uma ótima maneira de evitar tentar capturar cada const que você atribui await!
Jim
0

catchfazer isso, em minha experiência, é perigoso. Qualquer erro lançado em toda a pilha será detectado, não apenas um erro dessa promessa (que provavelmente não é o que você deseja).

O segundo argumento para uma promessa já é um retorno de rejeição / falha. É melhor e mais seguro usar isso.

Aqui está uma linha de texto typesafe que escrevi para lidar com isso:

function wait<R, E>(promise: Promise<R>): [R | null, E | null] {
  return (promise.then((data: R) => [data, null], (err: E) => [null, err]) as any) as [R, E];
}

// Usage
const [currUser, currUserError] = await wait<GetCurrentUser_user, GetCurrentUser_errors>(
  apiClient.getCurrentUser()
);
Sarink
fonte