Guide · AWS

Migrate AWS SDK for JavaScript from v2 to v3

AWS SDK for JavaScript v2 reached end-of-support on September 8, 2025. v3 splits the SDK into one package per service, replaces .promise() with send(), and changes how S3 bodies, credentials and errors work.

v2 still runs, but it no longer gets updates, including support for new services and features. v3 has been generally available since December 2020 and is a major rewrite: modular packages, a middleware stack, and TypeScript types. The good news is that service operation names and their input and output shapes are largely the same, so most changes are in how you create clients, call them and read results. This guide follows AWS's migration guide and the v3 UPGRADING notes, linked at the end.

What changed

  • One package per service (@aws-sdk/client-s3, @aws-sdk/client-dynamodb) instead of the single aws-sdk package.
  • Operations are commands sent through a client: await client.send(new GetObjectCommand(input)).
  • .promise() is gone; calls return promises.
  • S3 GetObject returns Body as a stream, not a Buffer. Read it with await Body.transformToString().
  • No global AWS.config: region and credentials are set on each client, and credential providers come from @aws-sdk/credential-providers.
  • Errors expose the error name and HTTP details through error.name and error.$metadata.
  • Helpers moved to separate packages: @aws-sdk/lib-storage, @aws-sdk/lib-dynamodb, @aws-sdk/s3-request-presigner.

Migration steps

  1. Install only the clients you use

    npm uninstall aws-sdk
    npm install @aws-sdk/client-s3 @aws-sdk/client-dynamodb

    AWS measures the full aws-sdk package at about 40 MB, a single v3 client at about 3 MB, and a client plus one command at under 100 KB. Import from the package root, not from submodule paths inside it.

  2. Create clients and send commands

    Before

    const AWS = require("aws-sdk");
    AWS.config.update({ region: "us-west-2" });
    
    const ddb = new AWS.DynamoDB();
    const data = await ddb.listTables({ Limit: 10 }).promise();
    console.log(data.TableNames);

    After

    import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb";
    
    const ddb = new DynamoDBClient({ region: "us-west-2" });
    const data = await ddb.send(new ListTablesCommand({ Limit: 10 }));
    console.log(data.TableNames);

    v3 also offers aggregated clients that keep the v2 method style: new DynamoDB({ region }) from the same package, then await ddb.listTables({ Limit: 10 }) with no .promise(). That is a smaller diff but pulls in every command for the service. The client plus command style is what AWS recommends for bundle size.

  3. Read S3 object bodies as streams

    Before

    const obj = await s3.getObject({ Bucket, Key }).promise();
    const text = obj.Body.toString("utf-8");

    After

    import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
    
    const s3 = new S3Client({ region: "us-west-2" });
    const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
    const text = await obj.Body?.transformToString();
  4. Set credentials per client

    Before

    AWS.config.update({
      region: "eu-west-1",
      credentials: new AWS.SharedIniFileCredentials({ profile: "dev" }),
    });
    const s3 = new AWS.S3();

    After

    import { S3Client } from "@aws-sdk/client-s3";
    import { fromIni } from "@aws-sdk/credential-providers";
    
    const s3 = new S3Client({
      region: "eu-west-1",
      credentials: fromIni({ profile: "dev" }),
    });

    If you set no credentials, v3 uses the default provider chain, in the same order as v2. ChainableTemporaryCredentials becomes fromTemporaryCredentials({ params: { RoleArn } }). For local S3 endpoints such as LocalStack, the v2 option s3ForcePathStyle is forcePathStyle in v3, next to endpoint.

  5. Update error handling

    Before

    try {
      await s3.getObject({ Bucket, Key }).promise();
    } catch (err) {
      if (err.code === "NoSuchKey") return null;
      console.error(err.statusCode);
      throw err;
    }

    After

    try {
      await s3.send(new GetObjectCommand({ Bucket, Key }));
    } catch (err) {
      if (err.name === "NoSuchKey") return null;
      console.error(err.$metadata?.httpStatusCode);
      throw err;
    }

    Service exceptions also support instanceof checks against the classes each client exports. The raw response is on err.$response.

  6. Replace the high-level helpers

    • s3.upload() and ManagedUpload: use Upload from @aws-sdk/lib-storage.
    • s3.getSignedUrl() and getSignedUrlPromise(): use getSignedUrl(client, command, { expiresIn }) from @aws-sdk/s3-request-presigner.
    • AWS.DynamoDB.DocumentClient: wrap a client with DynamoDBDocumentClient.from(client) from @aws-sdk/lib-dynamodb. v2 dropped undefined values for you; in v3 set removeUndefinedValues: true in marshallOptions to keep that behavior.
    • Waiters: import the one you need, for example await waitUntilBucketExists({ client, maxWaitTime: 60 }, { Bucket }).

How to find every affected call

Start by finding every import of v2, then every call style that changed. These ast-grep patterns match the syntax tree, so multi-line calls are found too:

sg run --pattern 'require("aws-sdk")' --lang js
sg run --pattern 'import $AWS from "aws-sdk"' --lang ts
sg run --pattern 'new AWS.$SERVICE($$$ARGS)' --lang ts
sg run --pattern '$CALL.promise()' --lang ts
sg run --pattern '$OBJ.Body.toString($$$ARGS)' --lang ts
sg run --pattern '$ERR.code === $CODE' --lang ts

Repeat with --lang js and --lang tsx as needed, and look for submodule imports such as aws-sdk/clients/s3. $CALL.promise() also matches non-AWS code that happens to have a .promise() method, and $ERR.code matches any error object, so review each hit. On Linux, sg may be a system command; ast-grep run is the same tool.

Testing the change

  • Rewrite mocks. Stubs on AWS.S3.prototype.getObject or helpers built for v2 do not intercept v3 calls. Mock send on the client, or use a v3-aware mocking library such as aws-sdk-client-mock.
  • Mocked S3 bodies must now be streams with transformToString(). A test that returns a Buffer will pass in a way production never will.
  • Run integration tests against a real account or LocalStack, with forcePathStyle set for local endpoints.
  • Check bundle size and cold start if you deploy to Lambda; the modular clients should make both smaller.

Automating it

AWS maintains an official codemod, aws-sdk-js-codemod. Preview with npx aws-sdk-js-codemod@latest --dry --print -t v2-to-v3 PATH..., then run without --dry --print to apply. It covers common cases such as imports, client creation and removing .promise(), and AWS asks you to review the changes thoroughly before deploying. Use it: it is free and fast.

What it does not do is prove the result. Versyka finds the affected call sites with ast-grep, rewrites them along with the code around them (body handling, error checks, mocks), and runs your repository's own install, typecheck, build and tests in a disposable sandbox before opening a pull request. If the checks fail, no pull request is opened.

Was this guide helpful?

Check your own repository

Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.

Sources