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.
The other, more subtle reason your Function ended with an application timeout is because of an incorrect invocation of 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
1exports.handler = (context, event, callback) => {2// Fetch the already initialized Twilio REST client3const client = context.getTwilioClient();45// Determine message details from the incoming event, with fallback values6const from = event.From || '+15095550100';7const to = event.To || '+15105550101';8const body = event.Body || 'Ahoy, World!';910client.messages11.create({ to, from, body })12.then((result) => {13console.log('Created message using callback');14console.log(result.sid);15// Callback is placed inside the successful response of the request16return callback();17})18.catch((error) => {19console.error(error);20// Callback is also used in the catch handler to end execution on errors21return callback(error);22});2324// If you were to place the callback() function here, instead, then the process would25// terminate before your API request to create a message could complete.26};
Example of how to return an empty HTTP 200 OK
1exports.handler = (context, event, callback) => {2// Providing neither error nor response will result in a 200 OK3return callback();4};
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.
1// An example of an improperly handled Promise2exports.handler = (context, event, callback) => {3const client = context.getTwilioClient();4client.messages.create({5body: "hi",6to: toNumber,7from: fromNumber,8});9return callback(null, 'success');10};
1// An example of properly waiting for message creation to2// finish before ending Function execution3exports.handler = (context, event, callback) => {4const client = context.getTwilioClient();5client.messages6.create({7body: 'hi',8to: event.toNumber,9from: event.fromNumber,10})11.then((message) => {12return callback(null, `Success! Message SID: ${message.sid}`);13});14// Ideally, you would also have some .catch logic to handle errors15};16
1// An example of properly waiting for message creation to2// finish before ending Function execution3exports.handler = async (context, event, callback) => {4const client = context.getTwilioClient();5const message = await client.messages.create({6body: 'hi',7to: event.toNumber,8from: event.fromNumber,9});1011return callback(null, `Success! Message SID: ${message.sid}`);12// Ideally, you would add some try/catch logic to handle errors13};
You can generate Voice TwiML using the Twilio Node library, which comes packaged within your Function.
1exports.handler = (context, event, callback) => {2const twiml = new Twilio.twiml.VoiceResponse()3twiml.dial().sip("sip:jack@example.com")4return callback(null, twiml);5}
1<?xml version="1.0" encoding="UTF-8"?>2<Response>3<Dial>4<Sip>sip:jack@example.com</Sip>5</Dial6</Response>
You can generate Messaging TwiML using the Twilio Node library, which comes packaged within your Function.
1exports.handler = (context, event, callback) => {2const twiml = new Twilio.twiml.MessagingResponse();3twiml.message('The Robots are coming! Head for the hills!');4callback(null, twiml);5}
1<?xml version="1.0" encoding="UTF-8"?>2<Response>3<Message>The Robots are coming! Head for the hills!</Message>4</Response>
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
1exports.handler = (context, event, callback) => {2// Construct an object in any way3const response = { result: 'winner winner!' };4// A JavaScript object as a response will be serialized to JSON and returned5// with the Content-Type header set to "application/json" automatically6return callback(null, response);7};
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:
1Access-Control-Allow-Origin: *2Access-Control-Allow-Headers: *3Access-Control-Allow-Methods: GET, POST, OPTIONS4
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:
1Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/your-name/your-project/node_modules/got/dist/source/index.js2require() of ES modules is not supported.3require() 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.4Instead 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.56at new NodeError (internal/errors.js:322:7)7at Object.Module._extensions..js (internal/modules/cjs/loader.js:1102:13)8at Module.load (internal/modules/cjs/loader.js:950:32)9at Function.Module._load (internal/modules/cjs/loader.js:790:12)10at Module.require (internal/modules/cjs/loader.js:974:19)11at require (internal/modules/cjs/helpers.js:101:18)12at Object. (/Users/your-name/your-project/functions/retry.js:2:13)13at Module._compile (internal/modules/cjs/loader.js:1085:14)14at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)15at 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:
1exports.handler = (context, event, callback) => {2import('got').then(({ default: got }) => {3got('https://httpbin.org/anything')4.json()5.then((response) => {6// ... the rest of your Function logic7});8});9};
However, it's recommended to leverage async/await syntax to handle the promise and minimize the amount of chaining/nesting necessary in your Function:
1exports.handler = async (context, event, callback) => {2const { default: got } = await import('got');34const response = await got('https://httpbin.org/anything').json();5// ... the rest of your Function logic6};
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:
const { 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:
1const somePackage = await import('some-package');23somePackage.default(); // Runs the default export from 'some-package'4somePackage.getPhoneNumbers();5somePackage.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.