There are two likely reasons your Function has completed with the error: 'runtime application timed out'.
The most common reason is that your Function has exceeded the 10-second execution time limit. You can determine this by looking at the execution logs on the Function instance page. The last log line after execution will tell you how many milliseconds the Function took to execute. If the processing time is greater than 10,000 milliseconds, then Twilio terminated your Function.
callback()
. If your Function does not call the callback()
method or the method is unreachable, your Function will continue executing until it reaches the time limit and ultimately fails. A very common mistake is to forget to capture the catch()
rejected state of a Promise
and calling callback()
there as well. The Function Execution documentation provides extensive details on the functionality and usage of the callback()
method. Below are several examples of correct use of callback()
to complete execution and emit a response.
Example of how to appropriately use callback() with an asynchronous HTTP request
_26exports.handler = (context, event, callback) => {_26 // Fetch the already initialized Twilio REST client_26 const client = context.getTwilioClient();_26_26 // Determine message details from the incoming event, with fallback values_26 const from = event.From || '+15095550100';_26 const to = event.To || '+15105550101';_26 const body = event.Body || 'Ahoy, World!';_26_26 client.messages_26 .create({ to, from, body })_26 .then((result) => {_26 console.log('Created message using callback');_26 console.log(result.sid);_26 // Callback is placed inside the successful response of the request_26 return callback();_26 })_26 .catch((error) => {_26 console.error(error);_26 // Callback is also used in the catch handler to end execution on errors_26 return callback(error);_26 });_26_26 // If you were to place the callback() function here, instead, then the process would_26 // terminate before your API request to create a message could complete._26};
Example of how to return an empty HTTP 200 OK
_10exports.handler = (context, event, callback) => {_10 // Providing neither error nor response will result in a 200 OK_10 return callback();_10};
The most common reason we have seen that a Function appears not to run is the misuse of callback
. Your Function invocation terminates when it calls callback
. If your request is asynchronous, for example, an API call to a Twilio resource, then you must call callback
after the success response of the request has resolved. This would be in the then
/catch
blocks chained to a request, or after a request that uses the await
keyword in an async
context.
Yes! Outgoing API requests from inside a Function (either directly via axios
/got
, or through an SDK such as Twilio's) can still be in an enqueued state even after the execution of a Function has ended.
This happens if you make an asynchronous request that returns a Promise, and forget to either await
it or chain on a .then
handler. Here, the Function may finish execution before the request occurs or completes. Then, your request may be deferred, sit idle, and run on a subsequent Function invocation in your Service before our system cleans it up.
Always be sure to wait for the Promises generated by your API calls and methods to resolve before invoking callback
. Below are examples of improperly and properly handled asynchronous logic, one which uses .then
chaining, and another using async
/await
syntax.
_10// An example of an improperly handled Promise_10exports.handler = (context, event, callback) => {_10 const client = context.getTwilioClient();_10 client.messages.create({_10 body: "hi",_10 to: toNumber,_10 from: fromNumber,_10 });_10 return callback(null, 'success');_10};
_15// An example of properly waiting for message creation to_15// finish before ending Function execution_15exports.handler = (context, event, callback) => {_15 const client = context.getTwilioClient();_15 client.messages_15 .create({_15 body: 'hi',_15 to: event.toNumber,_15 from: event.fromNumber,_15 })_15 .then((message) => {_15 return callback(null, `Success! Message SID: ${message.sid}`);_15 });_15 // Ideally, you would also have some .catch logic to handle errors_15};
_13// An example of properly waiting for message creation to_13// finish before ending Function execution_13exports.handler = async (context, event, callback) => {_13 const client = context.getTwilioClient();_13 const message = await client.messages.create({_13 body: 'hi',_13 to: event.toNumber,_13 from: event.fromNumber,_13 });_13_13 return callback(null, `Success! Message SID: ${message.sid}`);_13 // Ideally, you would add some try/catch logic to handle errors_13};
You can generate Voice TwiML using the Twilio Node library, which comes packaged within your Function.
Example of how to construct a Voice TwiML Response
You can generate Messaging TwiML using the Twilio Node library, which comes packaged within your Function.
Example of how to construct Messaging TwiML
Simply return your object as outlined in the Function Execution documentation or the following sample code:
Example of how to return JSON in HTTP 200 OK
_10exports.handler = (context, event, callback) => {_10 // Construct an object in any way_10 const response = { result: 'winner winner!' };_10 // A JavaScript object as a response will be serialized to JSON and returned_10 // with the Content-Type header set to "application/json" automatically_10 return callback(null, response);_10};
No, you cannot interact with the pre-flight OPTIONS request that browsers send.
The Runtime client will automatically respond to OPTIONS
requests with the following values:
_10Access-Control-Allow-Origin: *_10Access-Control-Allow-Headers: *_10Access-Control-Allow-Methods: GET, POST, OPTIONS
This means that all origins may access your Function, all headers are passed through, and that the only methods allowed to access your Function are GET
, POST
, and OPTIONS.
You can send CORS headers by using the Twilio Response object described in this example.
If you've tried to use a package such as got
or p-retry
in your Functions lately, you may have seen this error in your logs:
_15Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/your-name/your-project/node_modules/got/dist/source/index.js_15 require() of ES modules is not supported._15 require() of /Users/your-name/your-project/node_modules/got/dist/source/index.js from /Users/your-name/your-project/functions/retry.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules._15 Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /Users/your-name/your-project/node_modules/got/package.json._15_15 at new NodeError (internal/errors.js:322:7)_15 at Object.Module._extensions..js (internal/modules/cjs/loader.js:1102:13)_15 at Module.load (internal/modules/cjs/loader.js:950:32)_15 at Function.Module._load (internal/modules/cjs/loader.js:790:12)_15 at Module.require (internal/modules/cjs/loader.js:974:19)_15 at require (internal/modules/cjs/helpers.js:101:18)_15 at Object. (/Users/your-name/your-project/functions/retry.js:2:13)_15 at Module._compile (internal/modules/cjs/loader.js:1085:14)_15 at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)_15 at Module.load (internal/modules/cjs/loader.js:950:32)
This stems from the fact that packages in the Node.js ecosystem are migrating over from the old CommonJS (CJS) standard to the newer, ES Modules (ESM) standard. You can read about the differences in far more detail in this blog post.
At the moment, Functions only support CJS, but many packages such as p-retry
that you might want to use are now only exported as ESM. You could get around this by pinning to an older version of the library that is still CJS, but that opens you up to vulnerabilities and old, unsupported dependencies.
A better solution is to leverage dynamic imports, which allow you to import and run code from an ESM package even if your Function is still in CJS.
Using the import
method in this scenario returns a Promise, so you will need to properly handle that promise before running the rest of your Function's code. You can do so by nesting your Function's logic within a .then
chain, like so:
_10exports.handler = (context, event, callback) => {_10 import('got').then(({ default: got }) => {_10 got('https://httpbin.org/anything')_10 .json()_10 .then((response) => {_10 // ... the rest of your Function logic_10 });_10 });_10};
However, it's recommended to leverage async/await syntax to handle the promise and minimize the amount of chaining/nesting necessary in your Function:
_10exports.handler = async (context, event, callback) => {_10 const { default: got } = await import('got');_10_10 const response = await got('https://httpbin.org/anything').json();_10 // ... the rest of your Function logic_10};
Note in the above examples that you are destructuring the default export from the got
package, and renaming it to the intended name you'd use when importing it with a static require
statement. Refer to this guide on API Calls to see a full example of a Function using dynamic imports with an ESM package.
You can use this same strategy for packages that export multiple, named methods:
_10const { getPhoneNumbers, ahoy } = await import('some-package');
Lastly, you can import all methods from a package to a single variable, and access them by name like so:
_10const somePackage = await import('some-package');_10_10somePackage.default(); // Runs the default export from 'some-package'_10somePackage.getPhoneNumbers();_10somePackage.ahoy('hoy');
Currently, Functions are event-driven and can only be invoked by HTTP.
Absolutely! You can find a variety of examples in the navigation bar as well as on our Function Examples page.
During the Public Beta period, the UI editor can load and present up to 100 Functions or Assets by default. If a service has over 100 Functions or Assets, click on Load more to view the remaining resources, or the next 100 if there are more than that.
The Deploy All and Save buttons, and the ability to change Dependencies, will be disabled until all the Functions and Assets in the service have been loaded. Similarly, it is not possible to delete Functions or Assets until all the Functions or Assets are loaded for view.
To create and manage more Functions and/or Assets, we suggest using the Serverless Toolkit or the Functions and Assets API directly.
We limit Functions to 30 concurrent invocations — meaning that if you have over 30 Functions being invoked at the same time, you will see new Function invocations return with a 429 status code. To keep below the threshold, optimize your Functions to return as fast as possible — avoid artificial timeouts and use asynchronous calls to external systems rather than waiting on many API calls to complete.
Yes. See this blog post for a description and samples on how to store files for one-off usage.
Blog Posts around Asynchronous JavaScript techniques
The following Node.js runtimes are currently available for Twilio Functions:
runtime | Version | Status | Comments |
---|---|---|---|
node18 | Node.js 18.16.1 | available | Default for new Services on February 1, 2024 |
The runtime
value is a parameter that you can provide at build time. See an example on how to pass this parameter, or read the Node.js migration guide for more context.