Este artículo describe todos los tipos de errores que pueden ocurrir cuando se llama a un contrato inteligente, y cómo el bloque Try / Catch de Solidity responde (o no responde) a cada uno de ellos.
Para entender cómo funciona Try / Catch en Solidity, debemos comprender qué datos se devuelven cuando falla una llamada de bajo nivel. El compilador dicta este comportamiento, no la Ethereum Virtual Machine (EVM). Por lo tanto, los contratos escritos en lenguajes alternativos o en assembly no se adherirán necesariamente a todo el formato de errores explicado aquí.
Cuando una llamada de bajo nivel a un contrato externo falla, devuelve un valor booleano false. Este false indica que la llamada no se ejecutó con éxito. La llamada puede devolver false en los siguientes casos:
- El contrato llamado revierte
- El contrato llamado realiza una operación ilegal (como dividir por cero o acceder a un índice de array fuera de los límites)
- El contrato llamado consume todo el gas
Autodestruir un contrato no causa que la llamada de bajo nivel devuelva false en cadenas compatibles con la EVM que permiten la autodestrucción de contratos desplegados.
En las siguientes secciones, examinaremos 10 escenarios que podrían causar que una llamada de bajo nivel devuelva false, junto con cualquier dato de retorno que puedan proporcionar.
Luego exploraremos cómo Try / Catch maneja (o no maneja) cada situación.
Parte 1: Qué se devuelve durante un revert
1. ¿Qué se devuelve de un revert sin un string de error?
La forma más sencilla de usar revert es sin proporcionar una razón para el revert.
contract ContractA {
function mint() external pure {
revert();
}
}
Si desplegamos el contrato anterior (ContractA) y realizamos una llamada de bajo nivel a la función mint() desde otro contrato (ContractB) de la siguiente manera:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract ContractB {
function call_failure(address contractAAddress) external {
(, bytes memory err) = contractAAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err);
}
}
El error revert() se activará y no se devolverá ningún dato, como se muestra en la captura de pantalla a continuación:

En la imagen anterior, podemos ver que el dato de retorno del error es 0x, que es simplemente una notación hexadecimal sin datos adjuntos.
2. ¿Qué se devuelve de un revert con un string de error?
Otra forma de usar revert es proporcionando un mensaje de string. Esto ayuda a identificar por qué falló una transacción en tu contrato.
Activemos un revert con un string siguiendo nuestro ejemplo anterior y veamos qué se devuelve:
contract ContractB {
function mint() external pure {
revert("Unauthorized");
}
}
Y el contrato que realiza la llamada será:
import "hardhat/console.sol";
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err); // just so we can see the error data
}
}
Si desplegamos ambos contratos y ejecutamos ContractA con la dirección del contrato de ContractB, deberíamos obtener el siguiente resultado:

Cuando se activa revert con un argumento de string, devuelve la codificación ABI de la función Error Error(string) al llamador.
Los datos devueltos para nuestro revert serán la codificación ABI de la llamada a la función Error("Unauthorized").
En este caso, tendrá el selector de función de la función Error(string), el desplazamiento (offset) del string, la longitud y el contenido del string codificado en hexadecimal.

Expliquemos el resultado más a fondo:
- El selector
08c379a0son los primeros cuatro bytes dekeccak256("Error(string)")donde string se refiere al string de la razón. Los siguientes 96 bytes (3 líneas) son la codificación ABI del stringUnauthorized - Los primeros 32 bytes son el desplazamiento hacia la ubicación de la longitud del string.
- Los segundos 32 bytes son la longitud del string (12 bytes representados en hexadecimal como
c) - El contenido real del string
Unauthorizedestá codificado en UTF-8 como los bytes556e617574686f72697a6564
3. ¿Qué se devuelve de un revert personalizado?
Solidity 0.8.4 introdujo el tipo error que se puede usar con la declaración revert para crear errores personalizados que son legibles y eficientes en gas.
Para crear un tipo de error personalizado usarás la palabra clave error para definir un error, de manera similar a cómo defines eventos:
error Unauthorized();
También puedes definir errores personalizados con argumentos si necesitas enfatizar algunos detalles como parte de la información del error: error CustomError(arg1, arg2, etc).
error Unauthorized(address caller);
Revert personalizado sin argumentos
Comparemos un ejemplo de un revert personalizado con un argumento con uno que no lo tiene:
pragma solidity >=0.8.4;
error Unauthorized();
contract ContractA {
function mint() external pure {
revert Unauthorized();
}
}
En el ejemplo anterior, queremos revertir la transacción y devolver el error Unauthorized. Nuestro contrato que realiza la llamada seguirá siendo el mismo:
import "hardhat/console.sol";
contract ContractB {
function call_failure(address contractAAddress) external {
(, bytes memory err) = contractAAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err); // just so we can see the error data
}
}
El revert personalizado (sin argumentos) devolverá solo el selector de función (los primeros cuatro bytes de keccak256("Unauthorized()")) del error Unauthorized al llamador, que es 0x82b42900.

Revert personalizado con argumentos
Si tu revert personalizado tiene argumentos, devolverá la codificación ABI de la llamada a la función del error personalizado. Aquí tienes un ejemplo:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.4;
error Unauthorized(address caller);
contract ContractA {
function mint() external view {
revert Unauthorized(msg.sender);
}
}
El contrato que realiza la llamada seguirá siendo el mismo y el resultado del error se verá así:

Simplemente contiene la codificación ABI del error personalizado Unauthorized(address). La codificación incluye el selector de función y el argumento address. Dado que la dirección es un tipo estático, su codificación es directa.
Aquí está la estructura:
- Los primeros cuatro bytes representan el selector de función:
0x8e4a23d6 - Los siguientes 32 bytes representan la dirección del llamador:
0000000000000000000000009c84abe0d64a1a27fc82821f88adae290eab5e07
Como nota al margen, no puedes definir errores personalizados error Error(string) o error Panic(uint256) ya que estos entran en conflicto con los errores que require y assert devuelven respectivamente (llegaremos a assert en una sección posterior).
4. ¿Qué se devuelve de un revert debido a la declaración require?
La declaración require es otra forma de activar un revert sin usar una declaración if. Por ejemplo, en lugar de escribir:
if (msg.sender != owner) {
revert();
}
Puedes usar la declaración require de esta manera:
require(msg.sender == owner);
Cuando se llama a require(false) sin un mensaje de error, revierte la transacción sin datos, de manera similar a revert(). El resultado es una carga de datos (payload) vacía (0x).

De manera similar a revert con un string, cuando se activa un require con un string como require(false, "Unauthorized"), devuelve la codificación ABI de la función de error Error(string).
5. ¿Qué se devuelve de require(false, CustomError())?
Desde el 21 de mayo de 2024, se han lanzado errores personalizados para la declaración require; sin embargo, actualmente solo pueden ser usados mediante via-ir. (mira este video del equipo de Solidity describiendo via-ir).
via-iren Solidity es un pipeline de compilación que usa una representación intermedia (IR) en Yul para optimizar tu código Solidity. No está habilitado por defecto, por lo que necesitarás usar la bandera--via-irconsolco configurarlo en tu entorno de desarrollo preferido.
Habilitando via-ir en Foundry
Si estás usando Foundry, solo necesitas establecer via-ir en true en el archivo de configuración foundry.toml para activarlo de la siguiente manera:
[profile.default]
…
via-ir = true
Habilitando via-ir en Hardhat
En Hardhat, añade viaIR:true a tu archivo hardhat.config de la siguiente manera:
module.exports = {
solidity: {
settings: {
viaIR: true,
},
},
};
Habilitando via-ir en Remix
Si usas Remix, necesitarás habilitar el archivo de configuración en los ajustes de Configuraciones Avanzadas del Compilador (Advance Compiler Configurations), como se muestra en la captura de pantalla a continuación:

Crea un archivo compiler_config.json vacío en el directorio raíz. Y añade la ruta en la configuración como se muestra en la imagen anterior.
Una vez que la opción “Use configuration file” esté habilitada, actualiza el archivo de configuración para incluir "viaIR":true en los ajustes como se muestra a continuación. Puede que obtengas algunos errores del linter, pero tu código se compilará con éxito.

Una vez hecho esto, puedes escribir tu error personalizado con require de esta manera:
require(msg.sender == owner, Unauthorized());
Lo cual es lo mismo que esto:
if (msg.sender != owner) {
revert Unauthorized();
}
Y sí, devuelve el mismo resultado que el revert personalizado que ya discutimos.
6. ¿Qué se devuelve de un assert?
Cuando una declaración assert falla, desencadena un error Panic(uint256). El valor de retorno es la concatenación del selector de función (los primeros 4 bytes de keccak256("Panic(uint256)")) y el código de error.
El siguiente código se usará para ilustrar esto, nota el assert en ContractB:
import "hardhat/console.sol";
contract ContractB {
function mint() external pure {
assert(false); // we will test what this returns
}
}
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err);
}
}
Cuando desplegamos y ejecutamos el contrato, obtendremos el error del assert como se muestra a continuación:

El err contendrá los siguientes datos:
0x4e487b71 // <- the function selector
0000000000000000000000000000000000000000000000000000000000000001 // the error code
4e487b71 son los primeros cuatro bytes de keccak256("Panic(uint256)") donde uint256 se refiere al código de error. En este caso, el código de error es 1. Veremos otros códigos de error en la siguiente sección.
7. ¿Qué se devuelve de una operación ilegal?
Al igual que con la declaración assert, cuando ocurren operaciones ilegales como la división por cero, hacer pop a un array vacío o un error de índice fuera de límites en un array (array-out-of-bounds), la transacción entra en pánico (panics) y devuelve una concatenación del selector de función —los primeros 4 bytes de keccak256("Panic(uint256)")— y el código de error uint256.
Aquí tienes un ejemplo de una operación ilegal; array fuera de límites — la función outOfbounds() en el ContractB a continuación tiene solo 3 elementos en el array numbers.
import "hardhat/console.sol";
contract ContractB {
uint256[] numbers;
constructor() {
numbers.push(1);
numbers.push(2);
numbers.push(3);
}
function outOfbounds(uint256 index) public view returns (uint256) {
return numbers[index];
}
}
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("outOfbounds(uint256)", 10)
);
console.logBytes(err);
}
}
Si intentamos acceder al 10º elemento —que por supuesto, no existe, obtendremos el error de array fuera de límites:

El err contendrá:
0x4e487b71 //<- function selector for Panic(uint256)
0000000000000000000000000000000000000000000000000000000000000032 // <-the error code
0x32 es el código de error para el error de array fuera de límites.
Aquí hay otro ejemplo, ¿qué pasa si intentamos dividir por cero?
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractB {
function divide(uint256 a, uint256 b) public pure returns (uint256) {
return a / b;
}
}
…y luego llamamos a la función divide con los parámetros 10 y 0:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("divide(uint256, uint256)", 10, 0)
);
console.logBytes(err);
}
}
El resultado será el mismo selector de función, y el código de error para la división por cero que es 0x12.

8. ¿Qué devuelve la división por cero a nivel de Solidity vs. a nivel de assembly?
La división por cero a nivel de Solidity desencadena un revert con un código de error de 18 (0x12). Sin embargo, la división por cero a nivel de assembly no revierte, en su lugar devuelve 0. Eso es porque el compilador inserta verificaciones a nivel de Solidity, lo cual no se hace a nivel de assembly.

Si estás realizando una operación de división a nivel de assembly, asegúrate de comprobar el denominador. Si es cero, activa un revert para revertir la transacción.
function divideByZeroInAssembly(uint256 numerator, uint256 denominator)
public
pure
returns (uint256 result)
{
assembly {
if iszero(denominator) {
revert(0, 0)
}
result := div(numerator, denominator)
}
}
Pero este error no será manejado como una división por cero regular de Solidity que entra en pánico con un código de error de 18 en decimal o 0x12 en hexadecimal.
Si usas OpenZeppelin, puedes aprovechar la utilidad Panic personalizada de OZ para activar un Panic con tu código de error personalizado de esta manera:

Con esto, puedes simular el comportamiento normal de assert a nivel de Solidity.
Códigos de Error
Según la documentación de Solidity, los siguientes códigos de error se refieren a los diferentes tipos de pánicos que pueden ocurrir como se muestra en la captura de pantalla a continuación:

9. ¿Qué se devuelve durante un out-of-gas?
Durante una situación de error out-of-gas en una llamada de bajo nivel, no se devuelve nada al contrato llamador. Sin datos, sin mensajes de error.
contract E {
function outOfGas() external pure {
while (true) {}
}
}
contract C {
function call_outOfGas(address e) external returns (bytes memory err) {
(, err) = e.call{gas: 2300}(abi.encodeWithSignature("outOfGas()"));
}
}
La variable err estará vacía. Debido a la regla de 63/64 para el gas, al contrato C todavía le quedará 1/64 del gas original, por lo que la transacción hacia la función call_outOfGas en sí no revertirá necesariamente debido a la falta de gas, incluso si intentas enviar todo el gas disponible al contrato E.
10. ¿Qué se devuelve durante un revert en assembly?
Usar el revert en assembly te permite devolver datos de error de manera más eficiente en términos de gas comparado con el revert de Solidity.
revert en assembly toma dos parámetros: un espacio de memoria (memory slot) y el tamaño de los datos en bytes:
revert(startingMemorySlot, totalMemorySize)
Tú controlas exactamente qué datos de error se devuelven desde un revert en assembly. Por ejemplo, podemos elegir revertir usando el mensaje de error devuelto por un delegatecall usando returndatasize() para determinar el tamaño total de memoria de los datos devueltos, como hace el Proxy.sol de OpenZeppelin:
function _delegate(address implementation) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(
gas(),
implementation,
0,
calldatasize(),
0,
0
)
returndatacopy(0, 0, returndatasize())
if iszero(result) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
Usando assembly de bajo nivel, simulemos las declaraciones revert de Solidity y sus datos de retorno para obtener una mejor comprensión de la estructura de los datos devueltos en un revert en assembly.
Simulando un revert sin un string de razón
De manera similar a revert() en Solidity, revert(0,0) es el equivalente en Inline assembly. No devuelve ningún dato de error ya que el espacio de memoria inicial se define como 0, y tiene un tamaño de datos de 0, lo que indica que no se debe devolver ningún dato.
contract ContractB {
function revertWithAssembly() external pure {
assembly {
revert(0, 0) // no returned data
}
}
}
Simulando un revert con un string de razón
El revert con una razón en Solidity — revert(string) involucra varios pasos bajo el capó:
- Codificación ABI del
Error(string) - asignar memoria para almacenar los metadatos del string como la longitud y el desplazamiento (offset)
- y asignar memoria para el string real.
Todos estos pasos pueden aumentar los costos de gas.
Para optimizar el costo de gas, puedes lograr una funcionalidad similar usando assembly. Este método reduce los pasos y opcodes requeridos ya que sabemos y controlamos exactamente cómo se almacenan los datos, mientras seguimos devolviendo los mismos datos de error. En el ejemplo a continuación, manipulamos manualmente la memoria y almacenamos directamente:
- El selector de función de
Error(string)— podemos obtener el selector fuera del contrato y simplemente usarlo. Añadí la codificación en el ejemplo para mayor claridad. - El desplazamiento (offset)
- La longitud del string
- El string real
- Y activamos un revert
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractB {
function revertwithAssembly() external pure {
bytes4 selector = bytes4(abi.encodeWithSignature("Error(string)")); //selector with leading zeros
assembly {
mstore(0x00, selector) //- Store the function selector for `Error(string)`
mstore(0x04, 0x20) //- Store the offset to the error message string
mstore(0x24, 0xc) //- Store the length of the error message string
mstore(0x44, "Unauthorized") //- Store the actual error message
revert(0x00, 0x64) //- Trigger the revert revert(StartingMemorySlot, totalMemorySize)
}
}
}
Y cuando llamamos al contrato desde este contrato externo:
import "hardhat/console.sol";
contract ContractA {
function call_failure(address contractBAddress)
external
returns (bytes memory err)
{
(, err) = contractBAddress.call(
abi.encodeWithSignature("revertwithAssembly()")
);
console.logBytes(err);
}
}
El resultado serán los datos codificados en hexadecimal:

Lo cual es lo mismo que obtuvimos cuando usamos el revert(string) de Solidity.
Simulando un revert con error personalizado
Podemos simular un revert personalizado con assembly con el propósito de ahorrar más gas. Un revert personalizado en assembly devuelve el selector de función exactamente como el revert personalizado de Solidity.
Sin embargo, a diferencia de Solidity, donde ocurre la codificación completa del error personalizado, podemos eliminar ese paso y simplemente almacenar el selector directamente y activar el revert.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract ContractB {
function customRevertWithAssembly() external pure {
bytes32 selector = bytes32(abi.encodeWithSignature("Unauthorized()"));
assembly {
mstore(0x00, selector) //- Store the function selector for the custom error
revert(0x00, 0x04)
}
}
}
contract ContractA {
function call_failure(address contractBAddress)
external
returns (bytes memory err)
{
(, err) = contractBAddress.call(
abi.encodeWithSignature("customRevertWithAssembly()")
);
console.logBytes(err);
}
}
Deberíamos ver el selector como el valor devuelto cuando lo ejecutamos como se muestra a continuación:

Resumen de todas las formas en que un contrato Solidity puede revertir
Cuando una transacción revierte a través de una declaración require con un string de razón o un revert que contiene un string, el valor de retorno para el error es el selector de Error(string) seguido por la codificación ABI del string de razón.
Cuando una transacción revierte debido a un assert o una operación ilegal, los datos del error son el selector de Panic(uint256) seguido por el código de error codificado en ABI como un uint256.
Los datos de error están vacíos cuando:
- una transacción revierte con una declaración
require()orevert()sin string de razón - el contrato llamado agota todo el gas
- el contrato llamado usa assembly y revierte con
revert(0, 0)
Parte 2: Cómo maneja try/catch cada situación
En la primera sección de esta guía, hemos visto las diferentes maneras en que distintos reverts devuelven errores. Ahora, exploremos cómo la declaración try/catch en Solidity maneja cada una de estas situaciones.
La declaración try/catch proporciona una forma estructurada de manejar excepciones que pueden ocurrir durante llamadas a funciones externas o interacciones sin revertir y echar atrás toda la transacción. Sin embargo, los cambios de estado en el contrato llamado seguirán revirtiéndose si ocurre un error.
La declaración try/catch
Esta es la estructura típica de una declaración try/catch. Ten en cuenta que esto hace coincidencia de patrones (pattern matching) de todas las formas en que Solidity puede revertir:
function callContractB() external view {
try functionFromAnotherContract() {
//<-- Handle the success case if needed
} catch Panic(uint256 errorCode) {
//<-- handle Panic errors
} catch Error(string memory reason) {
//<-- handle revert with a reason
} catch (bytes memory lowLevelData) {
//<-- handle every other errors apart from Panic and Error with a reason
}
}
Los diferentes tipos de revert que discutimos anteriormente pueden ser capturados en diferentes secciones del bloque try/catch basándose en sus valores devueltos.
El bloque catch Error(string memory reason) maneja todos los reverts con un string de razón. Eso significa que los errores revert(string) y require(false, “reason”) serán capturados aquí. Esto se debe a que esos errores devuelven el error Error(string) cuando son activados.
El catch Panic(uint256 errorCode) capturará todas las operaciones ilegales, como la división por cero a nivel de Solidity, y los errores de assert ya que esos errores devuelven Panic(uint256 errorCode) cuando se activan.
Finalmente, cualquier otro error que no devuelva Panic o Error será capturado en el bloque catch genérico catch (bytes memory lowLevelData), incluyendo los errores personalizados y los errores sin un string de mensaje.
También puedes usar el bloque catch{ } si no estás interesado en los datos del error. Y esto capturará cualquier error del contrato llamado.
Echemos un vistazo a un ejemplo de la sintaxis try/catch. En este sencillo ejemplo, intentaremos simular los diferentes tipos de errores y escribir un try/catch para manejarlos según sus valores de retorno de error.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractB {
error CustomError(uint256 balance);
uint256 public balance = 10;
function decrementBalance() external {
require(balance > 0, "Balance is already zero");
balance -= 1;
}
function revertTest() external view {
if (balance == 9) {
// revert without a message
revert();
}
if (balance == 8) {
uint256 a = 1;
uint256 b = 0;
// This is an illegal operation and should cause a panic (Panic(uint256)) due to division by zero
a / b;
}
if (balance == 7) {
// revert with a message
revert("not allowed");
}
if (balance == 6) {
// revert with a message
revert CustomError(100);
}
}
}
Nuestra llamada al bloque try/catch para manejar estos errores se vería así:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
import {ContractB} from "contracts/revert/contractB.sol";
contract ContractA {
event Errorhandled(uint256 balance);
ContractB public contractB;
constructor(address contractBAddress) {
contractB = ContractB(contractBAddress);
}
function callContractB() external view {
try contractB.revertTest() {
// Handle the success case if needed
} catch Panic(uint256 errorCode) {
// handle illegal operation and `assert` errors
console.log("error occurred with this error code: ", errorCode);
} catch Error(string memory reason) {
// handle revert with a reason
console.log("error occured with this reason: ", reason);
} catch (bytes memory lowLevelData) {
// revert without a message
if (lowLevelData.length == 0) {
console.log("revert without a message occured");
}
// Decode the error data to check if it's the custom error
if ( bytes4(abi.encodeWithSignature("CustomError(uint256)")) == bytes4(lowLevelData)
) {
// handle custom error
console.log("CustomError occured here");
}
}
}
}
En cada uno de los bloques catch, hemos simulado el manejo de los diferentes tipos de errores. Para una mejor comprensión, he añadido comentarios explicando qué sucede en cada bloque.
Nota que no hay un bloque catch para errores personalizados como catch CustomError {}. En su lugar, lo manejamos en el bloque genérico catch-all con un proceso manual porque aún no hay una forma oficial de decodificar errores personalizados.
Hay un issue abierto sobre esto también con muchas sugerencias de cómo puedes hackearlo añadiendo declaraciones if en el bloque catch final para que coincida con el selector. El blog de lanzamiento de try/catch mencionó que hay un plan para mejorar la declaración try/catch para manejar errores personalizados adecuadamente en el futuro.

En nuestro caso, comprobamos si los datos de error de bajo nivel corresponden a la firma de un error personalizado específico que estamos intentando capturar.
if (bytes4(abi.encodeWithSignature("CustomError(uint256)")) == bytes4(lowLevelData)
){}
¿En qué escenarios try/catch fallará al manejar tus errores?
Dado que la sintaxis try / catch solo captura errores de contratos externos:
1) Cualquier error que ocurra dentro de un bloque try o catch (en el contrato llamador) no será capturado.
Por ejemplo, ninguno de los reverts en esta imagen será capturado (ejecutar en Remix):

2) Si el contrato no tiene el tipo correcto de “catch”
Por ejemplo, si el contrato revierte con panic pero solo tiene un bloque catch de error y no un bloque catch general, como se muestra en la captura de pantalla a continuación (ejecutar en Remix):

3) Revert cuando una interfaz espera datos de retorno pero no se proporciona ninguno
Si la interfaz que define la llamada al otro contrato espera datos de retorno y el contrato no devuelve ninguno, o los devuelve en un formato inesperado, la transacción completa revertirá y no será capturada, como se muestra en el ejemplo a continuación (ejecutar en Remix):

Problemas con try/catch en Solidity
Los problemas con try/catch han sido un tema de discusión con muchas proposiciones y hemos visto algunas de ellas en el transcurso de esta guía.
Los problemas destacados en la discusión incluyen:
Falsas expectativas creadas por la sintaxis
Existe la falsa idea de que la sintaxis try/catch funciona en Solidity como en otros lenguajes; como ya hemos visto, no es lo mismo. Por ejemplo, se esperaría que el siguiente flujo de código funcionara.
try <expression> {
revert();
} catch {
// also handle the revert in the try block
}
Pero no funcionará como se espera. El bloque catch no capturará el revert. Finalizará toda la transacción.
Falta de mecanismo para manejar reverts en verificaciones generadas por el compilador
Cuando hacemos llamadas de alto nivel a una función de otro contrato, el compilador de Solidity realiza varias verificaciones en el contrato llamado, tales como:
- Comprobar el
extcodesizedel contrato de destino para verificar si el destino es un contrato. Falla si la dirección no es un contrato. - Comprobar el
returndatasize— si se espera que el método devuelva algunos datos, verifica sireturndatasizeno está vacío. Si hay valores devueltos, los decodifica y valida que hayan sido codificados correctamente. - También realiza verificaciones de codificación y decodificación. El llamador también intentará decodificar por ABI los datos devueltos, y revertirá si los datos están mal formados o no existen.
Si cualquiera de estas verificaciones falla, la sintaxis try/catch no capturará el error.
Funciones faltantes
Aparte de catch Panic(uint256 errorCode) y catch Error(string memory reason), tener una funcionalidad que te permita capturar errores personalizados como catch CustomError() es una característica que se espera naturalmente en una sintaxis try/catch. Sin embargo, no existe sintaxis para esos casos de error, deben manejarse manualmente en el bloque catch.
Soluciones sugeridas a los problemas de try/catch en Solidity
A partir de la discusión de la propuesta que mencionamos antes, este es un breve resumen de las soluciones sugeridas que, al momento de escribir esto, no están implementadas:
- Extender la sintaxis
try/catchcon funcionalidades adicionales que definan explícitamente el tipo de error que estás manejando. Por ejemplo, el catchinternalmanejará reverts locales activados por verificaciones extra añadidas por el compilador (aún no capturará reverts activados dentro del mismo contrato), mientras que el catchexternalseguirá funcionando con la implementación catch existente. - Añadir nuevas cláusulas catch para reverts locales como:
- catch NoContract {}
- catch DecodingFailure {}
- catch Other {}
tryCall()ymatch— se espera que esta funcionalidad ejecute una coincidencia de patrones (pattern match) en la función externa y te permita manejar luego varios errores en diferentes ramas de la estructura match dependiendo del resultado y tipo de error. Aquí tienes un ejemplo de lapropuesta:
import { tryCall } from "std/errors";
error MyError(string);
match tryCall(token.transfer, (exampleAddress, 100)) {
CallSuccess(transferSuccessful) => {
...
}
CallFailure(MyError(reason)) => {
...
}
NotAContract => {
...
}
DecodingFailure(errorCode) => {
...
}
}
Esta propuesta se publicó en febrero de 2023. Así que esperamos ver qué enfoque gana y se implementa en el futuro.
Conclusión
Cuando un contrato de Solidity revierte, puede devolver un Error(string) codificado por ABI, un Panic(uint256), un error personalizado de 4 bytes, o nada en absoluto. El try-catch tiene catchs para manejar Error(string), Panic(uint256) y un catch general. No puede manejar errores personalizados de forma nativa.
Try catch falla si el llamador revierte o si el llamado (callee) devuelve datos en un formato que el llamador no espera (como intentar parsear datos vacíos o mal formados).
Autoría
Este artículo fue escrito por Eze Sunday en colaboración con RareSkills.
Publicado originalmente el 25 de julio de 2024