Evitando o erro “Cannot read properties of undefined”
- Lucas Akira
- Sem categoria
- abr 10, 2022
Algo muito comum quando estamos programando com JavaScript, principalmente quando estamos iniciando na programação, é nos depararmos com um erro parecido com esse:
TypeError: Cannot read properties of undefined (reading ‘property_name’)
E se você programa em outras linguagens como Java ou PHP, você também pode se deparar com as versões deles desse mesmo erro, no caso:
NullPointerException
no caso do JavaPHP Fatal error: Uncaught Error: Call to a member function function_name() on null
no caso do PHP
Por que esse erro acontece?
Esse erro acontece por causa de um único grande erro de design que acontece em diversas lingugens de programação conhecido como null
, sim esse tipo de dado é a causa desses problemas, e no Javascript temos um agravante pois temos um tipo de dados chamado undefined
que essêncialmente gera os mesmos problemas.
O problema é causado porque algumas funções retornam null para simbolizar que elas não estão retornando nada, quando geralmente nós esperamos um objeto, e muitas vezes não nos atentamos a esse fato de que o valor retornado poderia ser null e não o objeto que nós queriamos. Um exemplo que acontece com muita frequência é selecionar um elemento do DOM. Ex:
const counter = document.querySelector(".counter");
counter.addEventListener('click', () => {
counter.innerText = +counter.innerText + 1;
});
Note que se nenhum elemento com a class counter existir no HTML, o valor retornado pelo querySelector será null, e quando tentarmos chamar o método addEventListener nesse null, como null não tem nenhum método ou propriedade, ele vai dar um erro por não conseguir encontrar esse método addEventListener dentro de null.
Formas de resolver
Programação defensiva
A primeira e mais óbvia/simples é utilizarmos programação defensiva, o que significa que vamos ficar nos defendendo desses possíveis erros através de estruturas condicionais, no caso de funções que retornam null ou undefined, podemos simplemente adicionar um if antes de utilizar a variável, ex:
const counter = document.querySelector(".counter");
if (counter) {
counter.addEventListener('click', () => {
counter.innerText = +counter.innerText + 1;
});
}
Dessa forma ou ele vai ser null/undefined que são convertidos em false e não entram no if ou counter será um objeto que é convertido em true quando entra no if, e o código continua executando sem nenhum problema.
Optional chaining
Com a chegada do ES2020, o JavaScript recebeu uma nova feature muito boa conhecida como operador optional chaining, que é uma sintaxe que nos ajuda nesses casos onde queremos acessar um valor que é possívelmente nulo em um objeto, com ele ele faz essa verificação do if que fizemso no exemplo acima, e se o valor for null ele retorna null, e se não for ele retorna o valor da propriedade ou executa o método. Ex:
const counter = document.querySelector(".counter");
counter?.addEventListener('click', () => {
counter.innerText = +counter.innerText + 1;
});
Para mais detalhes sobre esse operador considere visitar a documentação do MDN sobre ele.
Usando um objeto Optional
A programação defensiva até funciona bem quando não temos como modificar a função para que ela possa retornar outra coisa, por exemplo o método querySelector (embora isso seja possível com monkey patching, mas isso é uma prática que deve ser evitada além de dificultar a tipagem).
Mas podemos lidar com isso de forma mais elegante encapsulando o valor em um objeto e então expondo métodos para lidar com ele no caso dele existir, algo muito parecido com o objeto Optional da linguagem Java por exemplo.
Então mãos na massa e vamos criar a nossa abstração para um valor opcional (ou seja ele pode existir ou não):
class Optional<T> {
/**
* Recebe o valor e guarda dentro do objeto
* na propriedade value.
*/
constructor(private value: T | null | undefined) {}
/**
* Verifica se o valor não existe no objeto
*/
isEmpty() {
return this.value === null || this.value === undefined;
}
/**
* Verifica se o valor existe no objeto
*/
isPresent() {
return !this.isEmpty();
}
/**
* Recebe uma função e passa o valor para que
* ela use ele caso ele exista senão ela não faz nada
*/
ifPresent(use: (value: T) => void) {
if (this.isPresent()) {
use(this.value);
}
}
/**
* Se o valor existir retorna ele, caso contrário retorna
* o valor passado pelo parâmetro
*/
or(value: T) {
return this.isPresent() ? this.value : value;
}
/**
* Pega o valor encapsulado ele existindo ou não.
*/
get() {
return this.value;
}
}
Ai podemos utiizar esse objeto desse jeito por exemplo:
const nullNumber = new Optional<number>(null);
nullNumber.ifPresent((number) => console.log(number)); // não mostra nada
console.log(nullNumber.or(0)); // 0
const validNumber = new Optional<number>(null);
validNumber.ifPresent((number) => console.log(number)); // 10
console.log(validNumber.or(0)); // 10
Ou podemos utilizar ele para fazer programação defensiva igual aos exemplos anteriores, ex:
const nullNumber = new Optional<number>(null);
if (nullNumber.isPresent()) {
console.log(nullNumber.get());
}
E para fechar, vamos dar uma olhada em como ficaria o nosso exemplo do querySelector usando nosso objeto Optional:
const maybeCounter = new Optional(document.querySelector(".counter") as HTMLButtonElement);
maybeCounter.isPresent((counter) => {
counter.addEventListener("click", () => {
counter.innerText = +counter.innerText + 1;
});
});
Conclusão
Eai, já sabia como resolver esse erro? Alguma das soluções te deu novas ideias para aplicar no seu dia-a-dia? Conta para gente aqui nos comentários, e até a próxima.