Skip to main content
Version: LOC v0.8 (legacy)

Tips on Error Handling

Some tips about handling errors in logic.

note

The tutorials is written for JavaScript and LOC Studio.

How Logic Handle Errors

In Logic and Session we've mentioned that each logic has a handleError() function for error handling. This is how it works:

  • LOC execute the run() function of a logic. If run() runs without issues, execute the run() of next logic in line.
  • If run() throws an error, handleError() in the same logic will be called.
  • All handleError() of the rest of logic (including aggregator) will be called. The original error will be passed down all the way to aggregator.
LogicExecuted function
Generic #1run()
Generic #2run() ❌ error thrown! -> handleError()
Generic #3handleError() (error originates from #2)
...handleError() (error originates from #2)
AggregatorhandleError() (error originates from #2)

In other words, if one logic fails, the rest of the logic will fail. The error would be passed down along a railway until it reaches the aggregator. This is to prevent a data pipeline causing more damage after something already went wrong. The task will still finish normally, but the result won't be the successful one.

Log and Execution History

Like we've seen in previous tutorials, we can use Logger Agent to log messages which can be read later in execution histories.

Since logs are very useful to trace how exactly logic in a data process has been executed, it is a good practice to log important information and actions in your logic.

Report Error to Trigger with Result Agent

If you want to report the error directly back to the trigger user, we can use Result Agent, which can also be used in handleError() to finalise a result containing the error:

Aggregator logic
import { ..., ResultAgent } from '@fstnetwork/loc-logic-sdk';

export async function run(ctx) {
// ...
}

export async function handleError(ctx, error) {
ResultAgent.finalize({
error: true,
errorMessage: error.message, // error passed down from other logic
stack: error.stack, // error stack (where did the error happened)
taskId: ctx.task.taskId,
// any other session data you'd like to send
});
}

This way, if the trigger returns a response containing an error field, you'll know something had gone wrong.

info

Since logic are executed by LOC runtime, error.stack may points the error origin to the LOC runtime backend instead of the logic script. These error are most likely agent-related, so check the SDK reference to see if you missed anything.

Handling Expected Errors

There may be situations that some errors are expected and are allowed be recovered. In this cases you can use JavaScript's try...catch...finally:

try {
// normal code (that might throw errors)
} catch (error) {
// handles error without triggering handleError()

// for example, log it with logging agent:
LoggingAgent.error(
`an error ${error.name} has occurred: ${error.message}, stack: ${error.stack}`,
);
} finally {
// optional; do something at the end no matter what
}

For example, querying a database might encounter problems like database failure or unstable connections:

let dbClient = null;

try {
dbClient = await DatabaseAgent.acquire("my-db-configuration");

const resp = await dbClient?.query(
"SELECT * FROM table1 WHERE col_1 = ? AND col_2 = ?;",
["value1", "value2"],
);

// other database operations
} catch (error) {
// report error
LoggingAgent.error(
`an error ${error.name} has occurred: ${error.message}, stack: ${error.stack}`,
);

// handle error
// ...
} finally {
await dbClient?.release(); // release DB client wether or not there are errors
}

Pass Errors to Aggregator Using Session

We can still report errors even if we are handling them quietly with the data process runs without issues.

The following example stores a caught error in a session variable called errorResult and have it to be picked up in the aggregator:

run() in generic logic
try {
// normal code (that might throw errors)
} catch (error) {
// generate an error message
const errorResult = {
error: true,
errorMessage: error.message,
stack: error.stack,
logicType: "Generic",
logic: ctx.task.currentLogic,
};

// log error
LoggingAgent.error(errorResult);

// write the updated err back to session
await SessionStorageAgent.putJson("errorResult", errorResult);
}
run() in aggregator logic
const errorResult = await SessionStorageAgent.get("errorResult");

if (!errorResult) {
// finalise a normal result
ResultAgent.finalize({
error: true,
errorResult: errorResult, // include errorResult to finalised result
taskId: ctx.task.taskId,
// ...
});
} else {
// if error exist in session, finalise the error report
ResultAgent.finalize(errorResult):
}
tip

Bear in mind that if an error occurred but not caught by try...error, the handleError() in aggregator logic will run instead.

Manually Throw an Error to Halt Data Process

Sometimes a certain error or user behavior is too severe to let go, and the data process has to be halted at all cost. You can deliberately throw an error to force the task fall back to the default handleError mechanism:

if (some_data == "some-invalid-value")
throw new Error(`error: some_data contains invalid value.`); // will invoke handleError() and make the task fail

You can also re-throw a caught error so that you can process it first, then embed the original error with additional descriptions:

try {
// ...
} catch (error) {
// create embedded error message
const err = `error: some_data contains invalid value. ${error.message}: ${error.stack}`;

// report the detail of the error to session
LoggingAgent.error(err);

// re-throw the error
throw new Error(err);
} finally {
// (optional) things to do whether or not an error had been occurred
}