Obtendo a mensagem de erro de um bloco catch com TypeScript

← ← ←   06/05/2022 17:15:05 | Postado por: Marlon E. Ruttmann


Disclaimer: este post é uma tradução deste post do blog do Kent C. Dodds

Certo, vamos falar sobre isso aqui:

  
    const reportError = ({message}) => {
      // send the error to our logging service...
    }
    
    try {
      throw new Error('Oh no!')
    } catch (error) {
      // we'll proceed, but let's report it
      reportError({message: error.message})
    }
  

Tranquilo até aqui? Claro, isso é só JavaScript. Vamos reescrever o trecho acima em TypeScript:

  
    const reportError = ({message}: {message: string}) => {
      // send the error to our logging service...
    }
    
    try {
      throw new Error('Oh no!')
    } catch (error) {
      // we'll proceed, but let's report it
      reportError({message: error.message})
    }
  

Há algo de errado na chamada de reportError. Especificamente em error.message. Isso porque (recentemente) o TypeScript tipa nosso error como unknown. E na verdade é exatamente isso que ele é! No mundo dos erros, não há muitas garantias que você pode oferecer quanto aos tipos de erros que serão lançados. De fato, este é o mesmo motivo pelo qual você não pode definir um tipo para o .catch(error => {}) de uma promise rejeitada com o generic Promise<ResolvedValue, NopeYouCantProvideARejectedValueType>. De fato, pode ser que nem mesmo um erro esteja sendo lançado. Pode ser simplesmente qualquer coisa:

  
    throw 'What the!?'
    throw 7
    throw {wut: 'is this'}
    throw null
    throw new Promise(() => {})
    throw undefined
  

É sério, você pode lançar qualquer de coisa de qualquer tipo. Então é fácil, certo? Podemos só adicionar uma anotação de tipo no erro para dizer que este código só vai lançar um erro, certo?

  
    try {
      throw new Error('Oh no!')
    } catch (error: Error) {
      // we'll proceed, but let's report it
      reportError({message: error.message})
    }
  

Não tão rápido! Deste modo você vai receber um erro de compilação do TypeScript:

Catch clause variable type annotation must be 'any' or 'unknown' if specified. ts(1196)

O motivo aqui é que, mesmo que em nosso código tenhamos a impressão de que não há como lançar qualquer outra coisa, o JavaScript é bem "liberal" e portanto é perfeitamente possível que uma biblioteca de terceiros faça algo "rebuscado" como um monkey-patching no construtor de Error para lançar algo diferente:

  
    Error = function () {
      throw 'Flowers'
    } as any
  

E o que um programador pode fazer então? O melhor que ele puder! Que tal isso:

  
    try {
      throw new Error('Oh no!')
    } catch (error) {
      let message = 'Unknown Error'
      if (error instanceof Error) message = error.message
      // we'll proceed, but let's report it
      reportError({message})
    }
  
There we go! Now TypeScript isn't yelling at us and more importantly we're handling the cases where it really could be something completely unexpected. Maybe we could do even better though:

E aí está! Agora o TypeScript não reclama mais e, mais importante, estamos tratando os casos onde ele poderia realmente causar algo completamente inesperado. Mas acho que talvez possamos fazer ainda melhor:

  
    try {
      throw new Error('Oh no!')
    } catch (error) {
      let message
      if (error instanceof Error) message = error.message
      else message = String(error)
      // we'll proceed, but let's report it
      reportError({message})
    }
  
Then we can turn this into a utility for use in all our catch blocks:

Aqui, se o error não for uma instância de Error, então podemos simplesmente fazer o stringify dele e confiar que o resultado disso nos será útil.

Assim, podemos transformar isso em um utilitário para usarmos em todos os nossos blocos catch

  
    function getErrorMessage(error: unknown) {
      if (error instanceof Error) return error.message
      return String(error)
    }
    
    const reportError = ({message}: {message: string}) => {
      // send the error to our logging service...
    }
    
    try {
      throw new Error('Oh no!')
    } catch (error) {
      // we'll proceed, but let's report it
      reportError({message: getErrorMessage(error)})
    }
  

Isso foi muito útil para mim em vários projetos. Espero que seja para você também!

Atualização: Nicolas deu uma boa sugestão para situaçoes onde objeto de erro não é realmente um erro. E então Jesse também sugeriu fazer um stringify do objeto do erro, se possível. As sugestões combinadas ficaram assim:

  
    type ErrorWithMessage = {
      message: string
    }
    
    function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
      return (
        typeof error === 'object' &&
        error !== null &&
        'message' in error &&
        typeof (error as Record).message === 'string'
      )
    }
    
    function toErrorWithMessage(maybeError: unknown): ErrorWithMessage {
      if (isErrorWithMessage(maybeError)) return maybeError
    
      try {
        return new Error(JSON.stringify(maybeError))
      } catch {
        // fallback in case there's an error stringifying the maybeError
        // like with circular references for example.
        return new Error(String(maybeError))
      }
    }
    
    function getErrorMessage(error: unknown) {
      return toErrorWithMessage(error).message
    }
  

Conclusão

Acho que o ponto chave é lembrar que, enquanto o TypeScript tem suas peculiaridades, não descarte um erro de compilação ou um warning do TypeScript só porque você acha que é impossível ou seja qual for o motivo. Na maioria das vezes é plenamente possível que ocorra o inesperado, e o TypeScript faz um grande trabalho lhe forçando a tratar estes casos improváveis... E você provavelmente perceberá que eles não são assim tão improváveis.