Handle Errors in Functions
On this page
This page explains how to work with errors in Atlas Functions.
Note
Custom Error Handling for Database Triggers Using AWS EventBridge
You can create a custom error handler specifically for database Triggers using AWS EventBridge. For more information, refer to Custom Error Handling.
Basic Error Handling
You can handle Function errors using standard JavaScript error handling techniques like try...catch statements.
function willThrowAndHandleError() { try { throw new Error("This will always happen"); } catch (err) { console.error("An error occurred. Error message:" + err.message); } } exports = willThrowAndHandleError;
View Logs
You can view records of all Function executions including which an error prevented successful execution in App Service Logs.
Depending on how a Function is invoked, it shows up differently in the logs. For example, logs for Functions called by Atlas Triggers show up in the logs as "Triggers" while logs for Functions called from a Realm client SDK show up in the logs as "Functions". For more information, refer to the Log entry type documentation.
Retry Functions
Atlas Functions do not have built-in retry behavior. You can add custom retry behavior. For example, you might want to add retry behavior if the third-party service that your Function calls has intermittent connectivity, and you want the Function to re-execute even if the third-party service is temporarily down.
This section describes the following strategies to add retry behavior to your Functions:
Recursively Call Function in Error Handling Blocks
You can handle operations that might fail by calling a Function recursively.
On a high-level, this process includes the following components:
Execute operations that you want to retry in a
try
statement and have the Function call itself in acatch
statement.To prevent indefinite execution, set a maximum number of retries. Every time the Function fails and enters the
catch
statement, increment a count of the current number of retries. Stop the recursive execution when the Function's current number of retries reaches the max number of retries.You may also want to throttle retries to reduce the total number of executions in a time frame.
The following table describes some advantages and disadvantages of handling Function retries with the recursive call strategy.
Advantages | Disadvantages |
---|---|
|
|
The following code example demonstrates an implementation of retrying a Function by using recursion in error-handling blocks.
// Utility function to suspend execution of current process async function sleep(milliseconds) { await new Promise((resolve) => setTimeout(resolve, milliseconds)); } // Set variables to be used by all calls to `mightFail` // Tip: You could also store `MAX_RETRIES` and `THROTTLE_TIME_MS` // in App Services Values const MAX_RETRIES = 5; const THROTTLE_TIME_MS = 5000; let currentRetries = 0; let errorMessage = ""; async function mightFail(...inputVars) { if (currentRetries === MAX_RETRIES) { console.error( `Reached maximum number of retries (${MAX_RETRIES}) without successful execution.` ); console.error("Error Message:", errorMessage); return; } let res; try { // operation that might fail res = await callFlakyExternalService(...inputVars); } catch (err) { errorMessage = err.message; // throttle retries await sleep(THROTTLE_TIME_MS); currentRetries++; res = await mightFail(...inputVars); } return res; } exports = mightFail;
Use Database Triggers to Retry
You can also retry Functions by using a Database Trigger to execute retries and a MongoDB collection to track previously-failed executions.
On a high-level, this process includes the following components:
Main Function that executes the logic you want to retry, wrapped in the handler function (see below bullet point).
Failed execution tracker MongoDB collection that tracks failed executions of the main Function.
Handler Function that invokes the main Function and logs when the function fails to the failed execution tracker collection.
Database Trigger Function that reruns the handler function whenever the handler function adds an error to the failed execution tracker collection.
You can support multiple main functions with one set of a handler Function, execution tracker collection, and Database Trigger Function.
Advantages | Disadvantages |
---|---|
|
|
Create a Function to handle execution retry
First, create the handler Function handleRetry
that invokes the main Function.
handleRetry
accepts the following parameters:
Parameter | Type | Description |
---|---|---|
functionToRetry | JavaScript Function | Function to retry. |
functionName | String | Name of the function you want to retry. |
operationId | ObjectId | Unique identifier for the main function's execution, including retries. |
previousRetries | Number | How many times the main function has previously been tried. |
...args | Rest parameters | Indefinite number of arguments passed to the main function. |
handleRetry
performs the following operations:
Attempts to execute
functionToRetry
in atry
statement. If the execution is successful,handleRetry
returns the value returned byfunctionToRetry
.If the execution of
functionToRetry
in the previous step throws an error, thecatch
statement handles the error as follows:Checks if the number of previous retries equals the maximum permitted number of retries. If the two numbers are the same, then the function throws an error because the max retries has been reached. The function no longer attempts to retry.
Build a function execution log entry object to insert into the database.
Get a reference to the failed execution tracker collection.
Insert the function log exection log entry into the failed execution tracker collection. This insertion operation causes the Database Trigger Function, which you will make in the next step, to fire.
The main function is passed as the argument functionToRetry
.
handleRetry
attempts to execute the main Function.
If the execution fails, this function attempts to retry the main function.
Navigate to Functions. Click the button Create New Function.
In the field Name, add handleRetry
.
In the Function Editor add the following code, then save the Function:
// Tip: You could also put this in an App Services Value const MAX_FUNC_RETRIES = 5; async function handleRetry( functionToRetry, functionName, operationId, previousRetries, ...args ) { try { // Try to execute the main function const response = await functionToRetry(...args); return response; } catch (err) { // Evaluates if should retry function again. // If no retry, throws error and stops retrying. if (previousRetries === MAX_FUNC_RETRIES) { throw new Error( `Maximum number of attempts reached (${MAX_FUNC_RETRIES}) for function '${functionName}': ${err.message}` ); } // Build function execution log entry for insertion into database. const logEntry = { operationId, errorMessage: err.message, timestamp: new Date(), retries: previousRetries + 1, args, functionName, }; // Get reference to database collection const executionLog = context.services .get("mongodb-atlas") .db("logs") .collection("failed_execution_logs"); // Add execution log entry to database await executionLog.insertOne(logEntry); return; } } exports = handleRetry;
Tip
Install and Set up App Services CLI
If you're using the CLI to update your App Services App, you must first install and set up the App Services CLI.
Add the following to functions/config.json
:
[ { "name": "handleRetry", "private": true, "run_as_system": true } // ...other configuration ]
Create the file for the Function functions/handleRetry.js
:
// Tip: You could also put this in an App Services Value const MAX_FUNC_RETRIES = 5; async function handleRetry( functionToRetry, functionName, operationId, previousRetries, ...args ) { try { // Try to execute the main function const response = await functionToRetry(...args); return response; } catch (err) { // Evaluates if should retry function again. // If no retry, throws error and stops retrying. if (previousRetries === MAX_FUNC_RETRIES) { throw new Error( `Maximum number of attempts reached (${MAX_FUNC_RETRIES}) for function '${functionName}': ${err.message}` ); } // Build function execution log entry for insertion into database. const logEntry = { operationId, errorMessage: err.message, timestamp: new Date(), retries: previousRetries + 1, args, functionName, }; // Get reference to database collection const executionLog = context.services .get("mongodb-atlas") .db("logs") .collection("failed_execution_logs"); // Add execution log entry to database await executionLog.insertOne(logEntry); return; } } exports = handleRetry;
Push your changes to App Services:
appservices push
Create retry database trigger
Navigate to the Triggers in the UI of your App.
Click the Add a Trigger button.
Create the Trigger with the following configuration:
FieldValueNameName of your choosing (ex:retryOperation
)EnabledYesSkip Events on Re-EnableYesEvent OrderingYesCluster NameName of your choosing (ex:mongodb-atlas
)Database NameName of your choosing (ex:logs
)Collection NameName of your choosing (ex:failed_execution_logs
)Operation TypeInsertFull DocumentYesDocument PreimageNoSelect An Event TypeFunctionFunctionClick + New Function. Refer to the following information about the contents of the function.Advanced ConfigurationNo advanced configuration necessary.
Add configuration for the Database Trigger. For more information, refer to the Trigger configuration reference.
{ "name": "retry", "type": "DATABASE", "config": { "operation_types": ["INSERT"], "database": "logs", "collection": "failed_execution_logs", "service_name": "mongodb-atlas", "project": {}, "full_document": true, "full_document_before_change": false, "unordered": false, "skip_catchup_events": false }, "disabled": false, "event_processors": { "FUNCTION": { "config": { "function_name": "retryOperationDbTrigger" } } } }
Now add the code for the Function that the Trigger invokes.
The function retryOperation
takes as a parameter logEntry
, the document that the
retry handler posted to the failed execution tracker collection.
Then, retryOperation
uses context.functions.execute()
to invoke the main function with information from logEntry
.
In the field Function Name, add retryOperationDbTrigger
.
For the field Function, add the following code, and then save the Trigger:
async function retryOperation({ fullDocument: logEntry }) { // parse values from log entry posted to database const { args, retries, functionName, operationId } = logEntry; // Re-execute the main function await context.functions.execute(functionName, ...args, operationId, retries); } exports = retryOperation;
Add the Function metadata to functions/config.json
:
[ // ...other configuration { "name": "retryOperationDbTrigger", "private": true } ]
Add the following code to the file functions/retryOperationDbTrigger.js
:
async function retryOperation({ fullDocument: logEntry }) { // parse values from log entry posted to database const { args, retries, functionName, operationId } = logEntry; // Re-execute the main function await context.functions.execute(functionName, ...args, operationId, retries); } exports = retryOperation;
Push your changes to App Services:
appservices push
Write Function to retry
Now that you have the function handler and the retry Database Trigger Function, you can write the main function.
In the following example, the Function randomly throws an error when performing addition. The JavaScript functions that execute this logic are the following:
getRandomOneTwoThree()
: Helper function for generating errors for the example.additionOrFailure()
: Function with the main logic.
The invocation of additionOrFailure()
wrapped by the retry handler
occurs in the exported function additionWithRetryHandler()
.
All functions that use the retry handler function should resemble this function.
You must include the correct parameters to make this function work with the rest of the retry logic. These parameters are:
Parameter | Type | Description |
---|---|---|
...args | Rest parameters | Zero or more parameters to pass to the function with main logic.
In the case of this example, the two numbers added
in additionOrFailure() , num1 and num2 . |
operationId | Unique identifier for the Function
call and retries. Set default value to a new BSON.ObjectId() . | |
retries | Number | Set default value to 0. |
The body of additionWithRetryHandler
is the retry handler handleRetry
invoked by context.functions.execute()
,
which in turn invokes additionOrFailure
. The arguments you pass to
context.functions.execute() are the following:
Argument | Type | Description |
---|---|---|
"handleRetry" | String | Name of the Function you defined to invoke the main function
and post to the retry logs if the main function doesn't properly execute. |
additionOrFailure | JavaScript function | The main function that handleRetry() invokes. |
operationId | BSON.ObjectId | Passed in as argument from the parameter operationId of additionWithRetryHandler() . |
retries | Number | Passed in as argument from the parameter retries of additionWithRetryHandler() . |
...args | Spread arguments | Zero or more arguments to pass to the function with main logic.
Passed in as argument from the parameter ...args of additionWithRetryHandler() |
In the field Function Name, add additionWithRetryHandler
.
For the field Function, add the following code and save the Function:
// randomly generates 1, 2, or 3 function getRandomOneTwoThree() { return Math.floor(Math.random() * 3) + 1; } function additionOrFailure(num1, num2) { // Throw error if getRandomOneTwoThree returns 1 const rand = getRandomOneTwoThree(); if (rand === 1) throw new Error("Uh oh!!"); const sum = num1 + num2; console.log(`Successful addition of ${num1} + ${num2}. Result: ${sum}`); // Otherwise return the sum return sum; } async function additionWithRetryHandler( inputVar1, inputVar2, // create a new `operation_id` if one not provided operationId = new BSON.ObjectId(), // count number of attempts retries = 0 ) { const res = await context.functions.execute( "handleRetry", additionOrFailure, "additionWithRetryHandler", // MUST BE NAME OF FUNCTION operationId, retries, inputVar1, inputVar2 ); return res; } exports = additionWithRetryHandler;
Add the Function metadata to functions/config.json
:
[ // ...other configuration { "name": "additionWithRetryHandler", "private": false } ]
Add the following code to the file functions/additionWithRetryHandler.js
:
// randomly generates 1, 2, or 3 function getRandomOneTwoThree() { return Math.floor(Math.random() * 3) + 1; } function additionOrFailure(num1, num2) { // Throw error if getRandomOneTwoThree returns 1 const rand = getRandomOneTwoThree(); if (rand === 1) throw new Error("Uh oh!!"); const sum = num1 + num2; console.log(`Successful addition of ${num1} + ${num2}. Result: ${sum}`); // Otherwise return the sum return sum; } async function additionWithRetryHandler( inputVar1, inputVar2, // create a new `operation_id` if one not provided operationId = new BSON.ObjectId(), // count number of attempts retries = 0 ) { const res = await context.functions.execute( "handleRetry", additionOrFailure, "additionWithRetryHandler", // MUST BE NAME OF FUNCTION operationId, retries, inputVar1, inputVar2 ); return res; } exports = additionWithRetryHandler;
Push your changes to App Services:
appservices push
Now when you invoke additionWithRetryHandler
,
the Function will retry if it fails.