Gabriel / Ramos

⟨ pintor de pixel ⟩

  • Blog
  • Depuração de aplicações JavaScript

Depuração de aplicações JavaScript

Como ir além do console.log e encontrar erros de forma mais eficiente

9 min. de leitura

Foto por The Nigmatic

Se você já escreveu algum trecho de código e percebeu que algo não funcionava como deveria, com certeza você já depurou uma aplicação.

Também conhecido como debugging (ou debugar), depurar é essa etapa de desenvolvimento onde você, através de ferramentas e funcionalidades da linguagem ou do seu editor/IDE, começa um processo de investigar e encontrar algum erro (ou bug) ou comportamento não desejado em seu software.

Particularmente acredito que quanto eficiente você for com as ferramentas de depuração que pode utilizar, mais facilmente você vai achar algum problema e investigar algo necessário em sua aplicação. O que faz com que você economize muito tempo e também evite ficar batendo cabeça sem necessidade.

O intuito desse post é compilar algumas dicas e utilitários que eu gosto de utilizar no meu dia-a-dia e que percebi que me tornam mais produtivo.

#debugger, breakpoints e os sourcemaps

Provavelmente o tópico que mais mudou a forma como eu realizo esse processo de debugging nas aplicações que eu encontro por aí.

A palavra chave debugger; é uma instrução que faz com que nosso código seja "pausado" (ou, em outras palavras, é um breakpoint) em um momento qualquer de execução. Podemos colocar

Vamos fazer um teste. Rode o seguinte código com o DevTools aberto:

function a() {
  var numero = 1;
  b();
}

function b() {
  var numero = 2;
  c();
}

function c() {
  var numero = 3;
  debugger;
}

a();

Assim que executar, você perceberá que o código será executado e seu DevTools pausará no momento em que encontra a declaração debugger;. Com isso, você consegue ter acesso às variáveis disponíveis nesse momento em que o breakpoint está ativo.

Com isso você também consegue clicar em alguma outra linha, criando outro breakpoint diretamente pelo DevTools, facilitando muito o trabalho de validar suas funções e variáveis em um determinado ponto no seu código.

Nesse momento de pausa você consegue visualizar alguns botões que permitem que você continue a execução da sua aplicação e até mesmo navegue entre linhas ou execuções de funções caso seja necessário.

Para aplicações que utilizam NodeJS, não basta apenas colocar uma instrução debugger;. Você também precisará executar a aplicação informando os argumentos --inspect ou --inspect-brk. Isso iniciará um processo que estará atrelado ao seu DevTools (preferencialmente o Google Chrome) de maneira que, após executá-lo, você verá o ícone do NodeJS.

A diferença entre --inspect e --inspect-brk é que o primeiro apenas inicia o processo atrelado ao navegador. Já o segundo, cria um breakpoint pra você automaticamente na primeira linha do seu código, caso você não tenha nenhuma instrução debugger; escrita ou queira navegar pelos arquivos do seu projeto antes.

Caso isso não ocorra, mesmo após executar o Node com esses valores, você pode dar uma olhada nas suas configurações do Google Chrome (na url chrome://inspect) e checar se as opções necessárias estão ativas.

#SourceMaps

No entanto, apenas utilizar debugger ou breakpoints pode não ajudar muito quando você lida com código comprimido/minificado, o que é muito comum.

Para isso existem os sourcemaps, aqueles arquivos com extensão .map que podem ser gerados por ferramentas de build. Esses arquivos guardam referencias aos arquivos fontes de projetos que utilizam ferramentas que trabalham com essas compressões/minificações ou qualquer outro processo de compilação (como TypeScript).

Eles servem para que, mesmo após processar algum código fonte, você consiga depurar o conteúdo escrito sem lidar com o código alterado (antes de processá-lo), manipulando através de breakpoints e "visualizando" diretamente o código que você escreveu.

Por exemplo, o seguinte código em TypeScript:

// arquivo index.ts
function test(a: number, b: number) {
  debugger;
  return a + b;
}

test(1, 2);

Irá gerar o conteúdo abaixo, após ser compilado:

// arquivo dist/index.js
"use strict";
function test(a, b) {
    debugger;
    return a + b;
}
test(1, 2);
//# sourceMappingURL=index.js.map

Note que no fim do conteúdo, existe um comentário com sourceMappingURL apontando para um arquivo index.js.map. Esse arquivo é um json contendo o seguinte:

{
  "version":3,
  "file":"index.js",
  "sourceRoot":"",
  "sources":["../src/index.ts"],
  "names":[],
  "mappings":";AAAA,SAAS,IAAI,CAAC,CAAS,EAAE,CAAS;IAC9B,QAAQ,CAAC;IACT,OAAO,CAAC,GAAG,CAAC,CAAC;AACjB,CAAC;AAED,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA"
}

E nesse conteúdo temos algumas informações importantes, como:

  • a versão (version) do sourcemap;
  • o arquivo (file) compilado que está relacionado ao sourcemap (index.js);
  • os arquivos fonte (sources) que geraram o sourcemap;
  • uma sequência de mapeamentos (mappings), que servem como base para a relação entre o código fonte e o código compilado.

Esses mapeamentos são gerados utilizando Base64 VLQ que é uma forma de codificar algum conteúdo, mas podemos ter em mente que são dados que relacionam o código compilado do arquivo file com o código fonte em sources.

Se quiser ler um pouco mais sobre Base64 VLQ e como esses mapeamentos são gerados, você pode ver esses 3 outros posts (em inglês):

Com essas referências e o arquivo sourcemap gerado corretamente, mesmo utilizado o debugger na aplicação, é possível ver pelo DevTools o código TypeScript que foi escrito e não o JavaScript que foi gerado apos a compilação.

Ah, vale comentar que esses arquivos de sourcemap só são baixados no dispositivo caso o DevTools esteja aberto, evitando o consumo desnecessário de recursos. O mesmo se aplica para códigos CSS que são processados por algum pré-processador ou algo do tipo.

#Funções do Console

O console é, de longe, um dos amigos mais antigos de quem escreve códigos em JavaScript e você provavelmente já o deve conhecer.

#log

A função mais conhecida de todas. Basta passar alguma mensagem ou variável para ele, que aparecerá no seu console:

const mensagem = 'Texto do console';

console.log(mensagem); // exibe 'Texto do console';

Dessa forma, sua mensagem é exibida corretamente.

#Logs formatados

O primeiro passo que podemos dar além do console tradicional, é formatar alguns logs conforme nossa necessidade. Você pode formatar visualmente um log adicionando o prefixo %c antes de sua mensagem e passando como segundo argumento do console uma string com regras de CSS.

Mais ou menos dessa forma:

const css = 'background: tomato; color: white;';
const mensagem = 'Texto do console';

console.log(`%c${mensagem}`, css); // exibe 'Texto do console';

Com isso, sua mensagem aparecerá igual anteriormente, mas com as regras de CSS aplicadas. Rode esse exemplo no console do seu navegador e veja o resultado.

No caso de aplicações com NodeJS, para mostrar seu log formatado as opções são um pouco diferentes. Você precisa informar os códigos das cores usando padrão ANSI.

Por exemplo, \x1b[32m deixa um texto verde. Após isso, precisamos concatenar %s para escrever nossa string e depois \x1b[0m que é o código que redefine as cores de log (para que os logs seguintes da aplicação não tenham o mesmo estilo aplicado). Tendo, ao final, algo como:

const cor = '\x1b[32m';
const texto = '%s';
const reset = '\x1b[0m';
const definicoes = `${cor}${texto}${reset}`;

const mensagem = 'Texto do console';

console.log(definicoes, mensagem);

Com isso, o texto ficará verde.

#error, info e debug

São outros três níveis de erro diferente. No caso do info a mensagem aparece com algumas marcações de informação diferente no navegador. error já exibe uma mensagem como se fosse um erro da aplicação (mas disparado pelo console) e debug

console.log('info');
console.error('error');
console.debug('debug');

#dir

Uma forma de exibir valores de forma mais organizada e com uma lista interativa. Por exemplo:

const pessoa = {
  nome: 'gabriel'
};

console.dir(pessoa);

Com isso, o objeto será exibido mas com uma formatação de lista e você pode clicar para expandir os valores internos.

#table

Exibe uma tabela com os valores do array/objeto informado:

const pessoa = {
  nome: 'gabriel'
};

console.table(pessoa);

#Algumas ferramentas mais voltadas à verificação de performance

Existem alguns utilitários do console que servem para medir o tempo de execução de um trecho de código. Claro que, como todo código JavaScript, esses valores mudaram de ambiente pra ambiente, dependendo bastante do processamento de cada máquina que executa um mesmo código.

#time, timeLog e timeEnd

São três funções utilizadas para criar um contador e verificar quanto tempo um trecho de código é utilizado. Todas elas (time, timeLog e timeEnd) levam necessitam de um texto que é utilizado como marcador desse contador criado. E elas funcionam da seguinte maneira:

  • time: inicia um contador com o marcador selecionado;
  • timeLog: exibe um log de algum marcador;
  • timeEnd: finaliza algum marcador criado.

Vamos fazer um exemplo para ficar mais claro. Vamos criar um timer com marcador loop, rodar um for 10 vezes e, após isso, finalizá-lo:

const marcador = 'loop';
console.time(marcador);

for(let i = 0; i < 10; i++) {
  console.timeLog(marcador, `rodou o laço no indice ${i}`);
}

console.timeEnd(marcador, 'finalizou loop');

Ao executar esse código, criamos um time com o marcador loop e temos algumas mensagens. Sendo elas:

  • 10 para a execução do laço;
  • 1 para a finalização do marcador time.

Cada uma delas com seu respectivo tempo de execução ao lado.

#performance API

Também disponível como um objeto global para ser verificado, performance é uma API que pode ser utilizada para verificar algumas informações como os valores de memória (memory), que apresenta informações sobre espaço de memória alocado e que pode ser utilizado em sua aplicação.

#copy

Um utilitário para quando você precisa copiar alguma estrutura para a área de transferência com o DevTools aberto que pouca gente usa.

Basta executar, no DevTools, algo como:

const estrutura = {
  pessoa: {
    nome: 'gabriel'
  }
};

copy(estrutura);

Que o objeto da variável estrutura estará na sua área de transferência. É bem prático e ajuda muito na hora de copiar alguma variável e jogar em um editor de texto para ver seus valores. Ajuda ainda mais quando você tem alguma resposta muito grande de uma requisição e precisa validar algum único campos.

#Boas investigações 🔎

Espero que algum desses utilitários ou estratégias possa te ajudar a investigar e encontrar algum problema de forma mais eficiente. Quem sabe até mesmo a estudar alguma aplicação nova por aí!


Compartilhe