avatarJean Cvllr

Summary

This context discusses the usage of the try/catch statement in Solidity for error handling in external function calls and contract creation.

Abstract

The text provides a comprehensive overview of the try/catch statement in Solidity, a feature added in version 0.6.2 to handle exceptions in external function calls and contract creation. It explains how to use the try/catch syntax and the optional return part. The context also covers different types of catch blocks, including generic catch blocks, retrieving Error(string) from a catch block, and important gotchas and culprits of try/catch. The article warns about the misconception that try/catch will handle all reverts, and emphasizes that only reverts in the try expression are caught. It also notes that changes in the callee will always be reverted, even if the entire transaction was successful.

Bullet points

  • Solidity supports exception handling with try/catch since version 0.6.2, but only for external function calls and contract creation.
  • The try keyword must be followed by an expression representing either an external function call or a contract creation.
  • Local variables defined inside the try or catch clauses are not accessible outside these clauses.
  • For external calls, the return part of the try statement is optional, even if the external function returns something.
  • If the returns is declared, it declares what is being returned by the external call, and a return variable name can be handled inside the try block.
  • In contract creation via try/catch, there are two options for using the return part: not defining it or defining it to retrieve the instance of the newly created contract.
  • There are currently four different types of catch blocks supported in Solidity, depending on the error type that was triggered on the callee.
  • If an error occurs in the expression used by the try statement, the execution will revert.
  • The try/catch syntax does not forward all the gas available, but only 63/64th of it.
  • The changes in the callee will always be reverted, even if the entire transaction was successful.

Solidity — All About Try / Catch

photo by Anthony Durant on Unsplash

This is Part IV of the All About Errors sub-series.

After looking at ways to handle errors at runtime, we will explore a specific type of error handling in Solidity: try {} catch {} . We will see how it can be used when doing to catch errors and creating new contracts.

We will look at how each type of Solidity runtime errors can be caught within the catch block using various syntax.

Table of content

- Introduction to Solidity try / catch
- How to use try / catch statement in Solidity?
- The optional "return" part of try / catch
    - option 1: do not defines the returns part
    - option 2: define a returns part
- Different types of catch blocks
- Generic catch { … } block
- Retrieving an Error(string) from a catch block
- Warning: Errors inside the try catch expressions are not caught!
- Important Gotchas and Culprits of try / catch

Introduction to Solidity Try Catch

Solidity supports exception handling with try / catch, but only for external function calls and contract creation calls.

The try catch syntax is available since version 0.6.2. It was added as a response to low-level calls, something many devs were already using.

In this case, the caller (a smart contract making the call) can react on failures and errors occurring in the callee (the smart contract being called) and react to such failures using the try / catch syntax.

How to use try / catch statement in Solidity?

The try keyword has to be followed by an expression representing:

  • either an external function call
  • or a contract creation → new ContractName()

It is also important to know that any local variables defined either inside the try or the catch clauses are not accessible outside these clause. They are scoped and accessible only inside the success or errors blocks they are defined in.

function tryCatchExternalCall(address target) public {
    try Target(target).doSomething() returns (string memory) {
            uint256 a = 1;
        } catch (bytes memory) {
            uint256 b = 2;
            // Does not compile
            // DeclarationError: Undeclared identifier
            uint256 c = a;
        }
        // Does not compile
        // DeclarationError: Undeclared identifier
        uint256 d = a;
        uint256 e = b;
    }
}

The (Optional) return part of try / catch

For external calls, the return part of the try statement is optional even if the external function you are trying to call returns something. See below for an example.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract TryCatchExamples {

    function tryCatchExternalCall(address target) public {

        try Target(target).doSomething() {

            // if external call was successful, we continue execution here
            // we do not care of what was returned.

        } catch {

        }

    }

}

contract Target {

    uint256 _callCounter;

    function doSomething() public returns (string memory) {
        _callCounter++;
        return "state updated successfully";
    }
}

If the returns is declared, it declares what is being returned by the external call (the type). You will usually declare a return variable name so that you can handle the return value inside the try { … } block.

try Target(target).doSomething() returns (string memory message) {
    // do something with `message` returned variable
} catch {

}

Note that the type declared inside the “inlined returns “ must match the return type defined in the function definition. To illustrate, the code snippet below will not compile and return the following error:

// TypeError: Invalid type, expected string memory but got bytes memory.
try Target(target).doSomething() returns (bytes memory message)

For contract creation via try / catch , there are two option on how to use the returns part.

option 1: do not defines the returns part

In this case, we only care that a contract was created successfully. If the deployment was successful, we can do whatever we want inside the try block.

function tryDeployingContract() public {
    try new Target() {
        // a new contract was created successfully.
        // we do not care about the address of the new contrat created
        // we can do whatever we want in the success block.
    } catch (bytes memory) {
    
    }
}

However, if we want to do things like retrieve the address of the newly created contract, we can achieve this by defining a returns statement as follow:

try new <Contract> returns (<Contract>)

option 2: define a returns part

Defining a returns part in the try block when doing contract creation via the new keyword enables to retrieve the instance of the newly created contract. The type inside the returns part must be the type of the contract we are deploying (see example below).

try new Target() returns (Target newContract) {
    // the return variable `newContract`
    // is a new instance of a `Target` contract
} catch (bytes memory) {

}

Even if we cannot return directly the address of the newly created contract inside the returns part, we can easily obtain it by explicitly converting from contract type to address (see code snippet below).

Once the new contract has been created and deployed successfully, and we have it’s instance accessible, we can do multiple things with this new contract instance inside the try block. Including:

  • making external call to this new contract.
  • read its balance
  • etc…

If the external call completes successfully without any errors, these variables are assigned and the contract execution continue within the try { ... } block (called the success block). Once the end of the success block is reached, execution in the smart contract continue skipping the catch { ... } block.

However, if something goes wrong in the external call or contract creation, execution enters the catch clause (the error block).

Different types of catch blocks

There are currently 4 different type of catch blocks supported in Solidity.

These 4 different types depend on the error type that was triggered on the callee (the contract that was called).

catch Error(string memory reason) { ... }

This catch clause is executed if the error was caused by revert("reasonString") or require(false, "reasonString") (or an internal error that causes such an exception).

catch Panic(uint errorCode) { ... }

If the error was caused by a panic, i.e. by a failing assert, division by zero, invalid array access, arithmetic overflow and others, this catch clause will be run.

catch (bytes memory lowLevelData) { ... }

This clause is executed if the error signature does not match any other clause, if there was an error while decoding the error message, or if no error data was provided with the exception. The declared variable provides access to the low-level error data in that case.

Generic catch { … } block

If you are not interested in the error data, you can just use catch { ... } (even as the only catch clause) instead of the previous clause.

This type of catch block will run regardless of the type of error (Panic(uint256), Error(string) , custom error , etc…).

Using

function tryCatchExternalCall(address target) public {

        try Target(target).doSomething() {

            // if external call was successful, we continue execution here
            // we do not care of what was returned.

        } catch {

            // this generic catch block will run regardless
            // of the type of error

        }

    }

In order to catch all error cases, you have to have at least the clause catch { ...} or the clause catch (bytes memory lowLevelData) { ... }. Therefore, the last two types are a form of “catch all” statements.

The official Solidity docs state the following:

Retrieving an Error(string) from a catch block

If an revert occur on the contract that was externally called, the revert reason string can be retrieved by the caller as follow:

try feed.getData(token) returns (uint v) {
    return (v, true);
} catch Error(string memory /*reason*/) {
    // This is executed in case
    // revert was called inside getData
    // and a reason string was provided.
}

Warning: Errors inside the try catch expressions are not caught!

There is an important gotcha with try {} catch {} to keep in mind. As stated in Solidity by the section in blue.

What does this mean? Let’s look at it closely with an example taken and documented in the Solidity forum.

function transferSomeTokens(address recipient) public {
    try token.transfer(recipient, currentBalance() - 100) returns (bool) {
        uint256 newBalance = token.balanceOf(recipient) 

        // this is a call to an internal function within this contract
        _registerTransfer(recipient, newBalance);    
    } catch {
        ...
    }
}

The current assumption of this code is that the try catch syntax will handle any revert, including the reverts occuring inside the try {} block.

This misconception come from the try catch syntax of other programming languages like Javascript. The equivalent Solidity code in JS could be written as follow using the JS try catch syntax.

try {
    const success: boolean = token.transfer(recipient, currentBalance() - 100); returns (bool)

    if (success) {
        const newBalance: number = token.balanceOf(recipient);
        _registerTransfer(recipient, newBalance);
    }
} catch {

}

In this JS/Typescript code snippet above, if something goes wrong when calling the internal method _registerTransfer(...) , an exception would immediately be caught and execution will switch to the catch block.

This is not the case in Solidity. In Solidity, when using the try <external-call> { ... } catch { ... } syntax, exception is only caught if something goes wrong in the external call.

If any revert occurs within the try {} block, whether it is:

  1. via the external call to token.balanceOf()
  2. via the internal method call to _registerTransfer(...)

→ the execution will revert ⚠️

→ the same apply if a revert occurs within the catch {} block.

→ and the same apply if the error occurs in the expression used by the try

The try {} catch {} syntax in Solidity does not make it obvious that this is the case. And a common expectation of users familiar with other languages is that catch will handle all reverts, both from external functions and issued by the current contract:

  • within the try block,
  • or within the catch block,
  • or within the <expression> called by the try statement.

Therefore, developers must be aware of this gotcha:

  1. do not expect that the try <expression> {} part also covers local reverts caused by the <expression>. It does not. If currentBalance() returns a value lower than 100 and the expression underflows, the control will not pass to the catch block. Instead the contract will revert.
  2. Second, the block where the balanceOf() call is performed is not covered. A revert in that call will not transfer control to catch, even though it’s an external call. Only reverts in the try expression are caught.

Important Gotchas & Culprits of try / catch

However one very important thing must be noted with the try / catch syntax: the changes in the callee will always be reverted.

This mean that in the case a failure occur on the callee and it revert, the caller can decide to not revert, handling the case in the catch block differently and return to complete a successful transaction. However, even if the entire transaction was successful, the changes in the callee if they reverted and ended up in the catch block will always be reverted.

The try catch syntax possess the issue that it does not forward all the gas available, but only 63/64th of it. This can be a problem in cases where the arguments to the function called in the try block are valid and the function being called should return successfully, but actually does not return because of a out of gas error. In this case, the execution will fall back in the catch block.

This issue is well documented in the tweet below:

https://blog.ethereum.org/2020/01/29/solidity-0.6-try-catch/

Ethereum
Solidity
Programming
Dapps
Blockchain
Recommended from ReadMedium