JavaScript: Tratando erros ao estilo Go

Como async/await nos ajuda a encapsular o nosso código

Eduardo Rabelo
Aug 29, 2018 · 4 min read
Cada linguagem com uma natureza diferente, será que é possível alguma semelhança?

A linguagem Go está cada vez mais em evidência. Uma das coisas que eu mais gosto nessa linguagem é a simplicidade do retorno de erros/respostas de funções que causam efeitos colaterais (side-effects).

Um exemplo bem bacana do pacote net/http:

func main() {
  res, err := http.Get("http://example.com/")
  if err != nil {
    // [A]
  }
  // [B]
}

Ou do pacote os, para lidar com sistema operacional:

func main() {
  file, err := os.Open("meu-arquivo.txt")
  if err != nil {
    // [A]
  }
  // [B]
}
  • [A]: cenário de erro da nossa função

Deixando os detalhes da linguagem de lado, afinal, aquilo ali é uma requisição assíncrona, certo? 😬

Será que poderíamos ter um estilo similar de respostas na nossa linguagem favorita?

Cenários em JavaScript

Historicamente falando, temos alguns cenários bem comuns em JavaScript para lidar com funções assíncronas:

Callbacks

Em Callbacks é comum passar uma função para o cenário de erro e sucesso:

function main() {
  $.ajax({
    url: "http://example.com/",
    success: function(data) {
      // cenário de sucesso
    },
    error: function(err) {
      // cenário de error
    }
  });
}
main();

Ao executarmos a função main(), temos um pequeno controle de quando podemos executar um código que depende do resultado dessa requisição. Obrigatoriamente precisamos colocar esse código dentro das funções das chaves success ou error.

Promises

Em Promises usamos o conceito de encadeamento de funções, utilizando os métodos .then e .catch:

function main() {
  return fetch("http://example.com/");
}
main()
  .then(res => {
    // cenário de sucesso
  })
  .catch(err => {
    // cenário de error
  });

Utilizando a função fetch, retornamos uma Promise na execução da função main() (perceba que estamos nomeando res ao invés de data, por padrão, fetch não irá processar a resposta para você). Novamente, temos um pequeno controle de quando podemos executar um código que depende do resultado dessa requisição. Obrigatoriamente precisamos colocar esse código dentro das funções .then ou .catch.

Error-first callbacks

Em Node.js, um cenário bem comum é retornar algum tipo de erro como o primeiro argumento da função Callback:

function main() {
  fs.readFile("meu-arquivo.txt", function(err, data) {
    if (err) {
      // cenário de sucesso
    }
    // cenário de error
  });
}
main();

Como nos cenário anteriores, temos um pequeno controle de quando podemos executar um código que depende do resultado dessa operação.

async/await

Introduzidos no ECMAScript 2017, as palavras chaves async e await, podem transformar declarações, expressões, definições ou funções em linha em um novo objeto onde uma Promise é sempre retornada.

Utilizando o exemplo de Promise acima, podemos atualizar nossa sintaxe:

async function main() {
  try {
    const res = await fetch("http://example.com/");
    // cenário de sucesso
  } catch (err) {
    // cenário de error
  }
}
await main(); // [A]

Um pouco melhor, não é? O código parece mais síncrono, ainda assim, temos que colocar o restante do nosso código dentro do try...catch e o tratamento de erro é separado da resposta de sucesso. Será que temos como melhorar isso?

Um pequeno detour

  • [A]: se você executar essa função (colocando uma URL válida) no Chrome DevTools, ela será executada sem problemas. Desde a versão 62, o Chrome suporta o chamado top-level await, onde você não precisa colocar uma função async em torno do await.

Porém, se quisermos executar esse código corretamente em um ambiente JavaScript/Node.js, precisamos de um pequeno truque:

async function main() {
  // código...
}
(async () => {
  await main();
})();

Ficou na dúvida? Compartilhe sua perguntar abaixo ;)

Encapsulando async/await

Utilizando nosso último exemplo do async/await, será que conseguimos encapsular ainda mais a execução daquela requisição de rede?

O nosso objetivo final aqui é:

  • receber na função main() o objeto de resposta e erro

Como nossa função main() já está sendo executada como uma função async, nós podemos mover todo o try…catch da requisição para uma função de serviço.

Essa função de serviço será responsável por executar a requisição e retornar a dupla objeto de resposta e error em um formato padrão para nossa função main().

Podemos atualizar nosso código para:

async function getExample() {
  try {
    const res = await fetch("http://example.com/");
    const data = await res.json();
    return [data, null];
  } catch (err) {
    return [null, err];
  }
}async function main() {
  const [data, err] = await getExample();
  if (err) {
    // cenário de error
  }
  // cenário de sucesso
}(async () => {
  await main();
})();

Hooray!!! 🎉🤗🎉

Ficou BEM semelhante ao Go, não? Apesar desse exemplo ser simples em sua natureza, o padrão de encapsular funções de domínio e criar uma interface de resposta padronizada para o side-effect, não é nova!

É possível deixar o seu projeto Node.js mais limpo, enxugar o tamanho das suas redux-thunk ou redux-saga e isolar boa parte do uso de try…catch (se não todos) da sua aplicação. Uma convenção única em toda a stack!

Podemos recapitular a solução final em 3 etapas:

  • Transforme a operação em uma Promise 🙏

Você que chegou até aqui, meu muito obrigado! 🤗

Eduardo Rabelo

Written by

☕🇳🇿 - https://eduardorabelo.me