How to handle errors when a transaction fail in React using Wagmi

In this tutorial, we are going to learn how to handle errors when you send a transaction from your React app using Wagmi but the transaction fails on the blockchain.

In this tutorial, we are going to learn how to handle errors when you send a transaction from your React app using Wagmi but the transaction fails on the blockchain.

If you send the transaction using Wagmi, with either the useSendTransaction hook or the useContractWrite hook, you can know, before sending the transaction, if it's going to fail or not and why.

Here is an example (one with the smart contract of the LINK token and another one with a transaction sending 1 ETH):

import { useSendTransaction, usePrepareSendTransaction, usePrepareContractWrite, useContractWrite } from 'wagmi';
import { utils } from 'ethers';

export function Home() {
  // Prepare the transaction
  const { config, error: contractWriteError } = usePrepareContractWrite({
    address: '0x514910771AF9Ca656af840dff83E8264EcF986CA',
    abi: ['function transfer(address _to, uint256 _value)'],
    functionName: 'transfer',
    args: [
      '0x5b8f1310A956ee1521A7bB56160451C786289aa9',
      utils.parseEther('200')
    ],
  });

  // Get the function to send the transaction
  const { data: writeContractData, write } = useContractWrite(config);

  const { config: sendTxConfig, error: sendTxError } = usePrepareSendTransaction({
    request: {
      to: '0x5b8f1310A956ee1521A7bB56160451C786289aa9',
      value: utils.parseEther('1'),
    },
  });

  const { data: sendTxData, sendTransaction } = useSendTransaction(sendTxConfig);

  console.log(contractWriteError, sendTxError);

  return (
    <div>
      {sendTxError && (
        <p>
          That transaction will fail for this reason:&nbsp;
          {sendTxError.reason}
        </p>
      )}
      {contractWriteError && (
        <p>
          Calling that contract function will fail for this reason:
          {contractWriteError.reason}
        </p>
      )}
      <button disabled={!sendTransaction} onClick={() => sendTransaction()}>
        Send transaction
      </button>
      <button disabled={!write} onClick={() => write()}>
        Write function
      </button>
    </div>
  );
}

As you can see, in the example above, we're able to know if the transactions are going to fail or not before sending them, using the error property of the hooks that prepare the transaction.

Both usePrepareSendTransaction and usePrepareContractWrite return an object that contains an error property.

That property is null when the transaction is expected to succeed. Otherwise it will be an object like this:

  • code: A string that represents what type of error it is. More information about it below
  • reason: A human-readable string of the reason why the transaction failed (it's only available for the error codes "CALL_EXCEPTION" and "TRANSACTION_REPLACED")
  • data: An hexadecimal string containing information about the error
  • transaction: An object containing the transaction data

Depending on the code of the error, the error object will have other properties:

  • CALL_EXCEPTION: this code means the transaction could not be validated on the blockchain. If you called a smart contract function and it reverted, this is the type of error that will be returned and the reason property will contain the message that was sent by the smart contract.

    For these types of errors, there will be 3 more properties:

     •  reason: a human-readable message containing the reason why the transaction failed

     •  method: the signature of the function that was called (name, parameters   and return value).

     •  args: an array that contains the arguments you sent to the smart contract   function.

  • INSUFFICIENT_FUNDS: the address that sent the transaction doesn't have enough funds to pay the gas fees or to send the Ethereum that the transaction sends

  • NETWORK_ERROR: There was an error on the network and the transaction couldn't be validated. For example, that can happen when the chain ID of the transaction is wrong which means the transaction was sent on the wrong network

  • NONCE_EXPIRED: The nonce passed to the transaction was already used in a previous transaction which is completed

  • REPLACEMENT_UNDERPRICED: This transaction failed because it was meant to replace another transaction but not enough gas was sent along with the transaction. You can try sending the transaction again and pass more gas.

  • TRANSACTION_REPLACED: This transaction has been replaced and cancelled by another transaction sent by the same address (a transaction with the same nonce but more gas was sent while this transaction was pending)

    For this type of errors, the error object will have the following extra properties:

     •  hash: the hash of the replaced transaction

     •  reason: One of "repriced", "cancelled" or "replaced".

     •  replacement: A TransactionResponse object containing information about the replacement transaction.

     •  receipt: the TransactionReceipt of the transaction the replacement this transaction

     •  cancelled: if true, the reason property will be "cancelled" or "replaced" but if it's false the reason property will be "repriced"

  • UNPREDICTABLE_GAS_LIMIT: The node where you created the transaction could not estimate the gas needed for this transaction. That happens when the node knows the transaction will fail before sending it.
    If it happens, there will be a reason property indicating why the transaction failed.

That same error object is returned when you wait for a transaction using the useWaitForTransaction hook or the .wait function in the data property of useSendTransaction or useContractWrite.

Here is an example using the .wait method:

const { data } = useSendTransaction(config)

useEffect(() => {
  async function waitAndHandleErrors() {
    data
      .wait(1)
      .then((txReceipt) => {
        if (txReceipt.status == 1) {
          console.log('The transaction was successful');
          console.log(txReceipt);
        }
        else {
          console.log("the transaction failed")
        }
      })
      .catch((error) => {
        console.log('The transaction failed:', error);
        if (error.code === 'INSUFFICIENT_FUNDS') {
          console.log('not enough funds to pay for gas fees');
        }

        if (error.code === 'NETWORK_ERROR') {
          console.log("could not validate transaction, check that you're on the right network");
        }

        if (error.code === 'TRANSACTION_REPLACED') {
          console.log('the transaction was replaced by another transaction with the same nonce');
        }
      });
  }

  if (data?.wait) waitAndHandleErrors();
}, [data]);

The error object in the catch block will be the same as the one described above.

Here is another example using the useWaitForTransaction hook:

const { data: receipt, error, sendTransaction } = useSendTransaction(config)

Again here, the error object will be the same as the one described above.

So to summarize everything we've just talked about, this is how you can handle errors when sending transactions:

  1. When you prepare the transaction using the usePrepareSendTransaction or usePrepareContractWrite hooks, the error object returned will tell you if the transaction you want to send will fail or not and why
  2. When you sent a transaction and you're waiting for it to complete using wait, you can catch the error thrown by that this function when the transaction fails and it will tell you why it failed
  3. When you sent a transaction and you're waiting for it using useWaitForTransaction, you can use the error property returned by that hook when the transaction fails (if it fails) to know the reason why it failed.

Lastly, if you want to get the reason why a transaction you didn't sent failed, you can get it using the useTransaction hook and then call .wait on the data returned and handle the errors like we've done above and know the reason why it failed.

And that's it 🎉

Thank you for reading this article