├── .npmrc ├── .gitattributes ├── .gitignore ├── .editorconfig ├── index.test-d.ts ├── .github └── workflows │ └── main.yml ├── index.js ├── browser.js ├── license ├── package.json ├── index.d.ts ├── readme.md ├── test-browser.js └── test.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import {promiseStateAsync, promiseStateSync, type PromiseState} from './index.js'; 3 | 4 | const promise = Promise.resolve(); 5 | 6 | expectType>(promiseStateAsync(promise)); 7 | expectType(promiseStateSync(promise)); 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 24 14 | - 20 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions/setup-node@v5 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {inspect} from 'node:util'; 2 | 3 | function getPromiseState(promise) { 4 | if (!(promise && typeof promise === 'object' && typeof promise.then === 'function')) { 5 | throw new TypeError(`Expected a promise, got ${typeof promise}`); 6 | } 7 | 8 | const inspectedString = inspect(promise, {breakLength: Number.POSITIVE_INFINITY}); 9 | 10 | if (inspectedString.includes('')) { 11 | return 'pending'; 12 | } 13 | 14 | if (inspectedString.includes('')) { 15 | return 'rejected'; 16 | } 17 | 18 | return 'fulfilled'; 19 | } 20 | 21 | export async function promiseStateAsync(promise) { 22 | // In Node.js, use util.inspect for inspection without attaching handlers 23 | return getPromiseState(promise); 24 | } 25 | 26 | export function promiseStateSync(promise) { 27 | return getPromiseState(promise); 28 | } 29 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | export async function promiseStateAsync(promise) { 2 | if (!(promise && typeof promise === 'object' && typeof promise.then === 'function')) { 3 | throw new TypeError(`Expected a promise, got ${typeof promise}`); 4 | } 5 | 6 | let isSettled = false; 7 | let state = 'pending'; 8 | 9 | // eslint-disable-next-line promise/prefer-catch 10 | promise.then( 11 | () => { 12 | isSettled = true; 13 | state = 'fulfilled'; 14 | }, 15 | error => { 16 | isSettled = true; 17 | state = 'rejected'; 18 | throw error; // Rethrow so unhandled rejection still fires 19 | }, 20 | ); 21 | 22 | // Wait for handlers to run (need to flush microtask queue) 23 | await Promise.resolve(); 24 | await Promise.resolve(); 25 | 26 | return isSettled ? state : 'pending'; 27 | } 28 | 29 | export function promiseStateSync(_promise) { 30 | throw new Error('This method is not available in the browser'); 31 | } 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p-state", 3 | "version": "3.0.0", 4 | "description": "Inspect the state of a promise", 5 | "license": "MIT", 6 | "repository": "sindresorhus/p-state", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "node": "./index.js", 17 | "default": "./browser.js" 18 | }, 19 | "sideEffects": false, 20 | "engines": { 21 | "node": ">=20" 22 | }, 23 | "scripts": { 24 | "test": "xo && node --test test.js && tsd" 25 | }, 26 | "files": [ 27 | "index.js", 28 | "index.d.ts", 29 | "browser.js" 30 | ], 31 | "keywords": [ 32 | "promise", 33 | "state", 34 | "inspect", 35 | "inspection", 36 | "reflect", 37 | "reflection", 38 | "introspect", 39 | "introspection", 40 | "pending", 41 | "fulfilled", 42 | "rejected" 43 | ], 44 | "devDependencies": { 45 | "delay": "^6.0.0", 46 | "tsd": "^0.33.0", 47 | "xo": "^1.2.2" 48 | }, 49 | "xo": { 50 | "rules": { 51 | "promise/prefer-await-to-then": "off" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type PromiseState = 'pending' | 'fulfilled' | 'rejected'; 2 | 3 | /** 4 | Asynchronously inspect the state of a promise. 5 | 6 | Note: While this is async, it does return the state in the next [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide), which is almost right away. 7 | 8 | @param promise - The promise to inspect. 9 | @returns A promise for the promise state. 10 | 11 | @example 12 | ``` 13 | import timers from 'node:timers/promises'; 14 | import {promiseStateAsync} from 'p-state'; 15 | 16 | const timeoutPromise = timers.setTimeout(100); 17 | 18 | console.log(await promiseStateAsync(timeoutPromise)); 19 | //=> 'pending' 20 | 21 | await timeoutPromise; 22 | 23 | console.log(await promiseStateAsync(timeoutPromise)); 24 | //=> 'fulfilled' 25 | ``` 26 | */ 27 | export function promiseStateAsync(promise: Promise): Promise; 28 | 29 | /** 30 | Synchronously inspect the state of a promise. 31 | 32 | Note: This method does not work in the browser. 33 | 34 | @param promise - The promise to inspect. 35 | @returns The promise state. 36 | 37 | @example 38 | ``` 39 | import timers from 'node:timers/promises'; 40 | import {promiseStateSync} from 'p-state'; 41 | 42 | const timeoutPromise = timers.setTimeout(100); 43 | 44 | console.log(promiseStateSync(timeoutPromise)); 45 | //=> 'pending' 46 | 47 | await timeoutPromise; 48 | 49 | console.log(promiseStateSync(timeoutPromise)); 50 | //=> 'fulfilled' 51 | ``` 52 | */ 53 | export function promiseStateSync(promise: Promise): PromiseState; 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # p-state 2 | 3 | > Inspect the state of a promise 4 | 5 | You would usually not need this as you can just `await` the promise at any time to get its value even after it's resolved. This package could be useful if you need to check the state of the promise before doing a heavy operation or for assertions when writing tests. 6 | 7 | **Vote up [this issue](https://github.com/nodejs/node/issues/40054) if you want to see this feature being included in Node.js itself.** 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install p-state 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | import timers from 'node:timers/promises'; 19 | import {promiseStateSync} from 'p-state'; 20 | 21 | const timeoutPromise = timers.setTimeout(100); 22 | 23 | console.log(promiseStateSync(timeoutPromise)); 24 | //=> 'pending' 25 | 26 | await timeoutPromise; 27 | 28 | console.log(promiseStateSync(timeoutPromise)); 29 | //=> 'fulfilled' 30 | ``` 31 | 32 | ## API 33 | 34 | ### `promiseStateAsync(promise: Promise)` 35 | 36 | Asynchronously inspect the state of a promise. 37 | 38 | Returns a promise for the state as a string with the possible values: `'pending'`, `'fulfilled'`, `'rejected'`. 39 | 40 | Note: While this is async, it does return the state in the next [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide), which is almost right away. 41 | 42 | ```js 43 | import timers from 'node:timers/promises'; 44 | import {promiseStateAsync} from 'p-state'; 45 | 46 | const timeoutPromise = timers.setTimeout(100); 47 | 48 | console.log(await promiseStateAsync(timeoutPromise)); 49 | //=> 'pending' 50 | 51 | await timeoutPromise; 52 | 53 | console.log(await promiseStateAsync(timeoutPromise)); 54 | //=> 'fulfilled' 55 | ``` 56 | 57 | ### `promiseStateSync(promise: Promise)` 58 | 59 | Synchronously inspect the state of a promise. 60 | 61 | Returns the state as a string with the possible values: `'pending'`, `'fulfilled'`, `'rejected'`. 62 | 63 | Note: This method does not work in the browser. 64 | 65 | ## Related 66 | 67 | - [p-reflect](https://github.com/sindresorhus/p-reflect) - Make a promise always fulfill with its actual fulfillment value or rejection reason 68 | - [p-settle](https://github.com/sindresorhus/p-settle) - Settle promises concurrently and get their fulfillment value or rejection reason 69 | - [More…](https://github.com/sindresorhus/promise-fun) 70 | -------------------------------------------------------------------------------- /test-browser.js: -------------------------------------------------------------------------------- 1 | // Need to test manually in devtools 2 | // $ npx esbuild test-browser.js --bundle | pbcopy 3 | import {promiseStateAsync} from './browser.js'; 4 | 5 | // eslint-disable-next-line unicorn/prefer-top-level-await 6 | (async () => { 7 | let passed = 0; 8 | let failed = 0; 9 | 10 | function assert(actual, expected, description) { 11 | if (actual === expected) { 12 | console.log('✅', description, `(${actual})`); 13 | passed++; 14 | } else { 15 | console.error('❌', description, `- expected: ${expected}, got: ${actual}`); 16 | failed++; 17 | } 18 | } 19 | 20 | console.log('Testing promiseStateAsync in browser...\n'); 21 | 22 | // Test pending promise 23 | const pendingPromise = new Promise(() => {}); 24 | assert(await promiseStateAsync(pendingPromise), 'pending', 'Pending promise'); 25 | 26 | // Test fulfilled promise 27 | const fulfilledPromise = Promise.resolve('value'); 28 | assert(await promiseStateAsync(fulfilledPromise), 'fulfilled', 'Fulfilled promise'); 29 | 30 | // Test rejected promise 31 | const rejectedPromise = Promise.reject(new Error('test error')); 32 | assert(await promiseStateAsync(rejectedPromise), 'rejected', 'Rejected promise'); 33 | rejectedPromise.catch(() => {}); // Handle to prevent unhandled rejection 34 | 35 | // Test promise that resolves after a delay 36 | const delayedPromise = new Promise(resolve => { 37 | setTimeout(resolve, 100); 38 | }); 39 | assert(await promiseStateAsync(delayedPromise), 'pending', 'Delayed promise (before)'); 40 | await delayedPromise; 41 | assert(await promiseStateAsync(delayedPromise), 'fulfilled', 'Delayed promise (after)'); 42 | 43 | // Test Promise.all with pending 44 | const allPromise = Promise.all([Promise.resolve(1), new Promise(() => {})]); 45 | assert(await promiseStateAsync(allPromise), 'pending', 'Promise.all with pending'); 46 | 47 | // Test Promise.race with resolved 48 | const racePromise = Promise.race([new Promise(() => {}), Promise.resolve('fast')]); 49 | assert(await promiseStateAsync(racePromise), 'fulfilled', 'Promise.race with resolved'); 50 | 51 | // Test async function that never resolves 52 | async function asyncFunction() { 53 | await new Promise(() => {}); // Never resolves 54 | } 55 | 56 | const asyncPromise = asyncFunction(); 57 | assert(await promiseStateAsync(asyncPromise), 'pending', 'Async function (pending)'); 58 | 59 | // Test thenable-like object 60 | const thenableLike = { 61 | // eslint-disable-next-line unicorn/no-thenable 62 | then(resolve) { 63 | resolve('thenable value'); 64 | }, 65 | }; 66 | assert(await promiseStateAsync(thenableLike), 'fulfilled', 'Thenable-like object'); 67 | 68 | // Test invalid input 69 | try { 70 | await promiseStateAsync(null); 71 | console.error('❌ Should have thrown for null input'); 72 | failed++; 73 | } catch (error) { 74 | if (error.message === 'Expected a promise, got object') { 75 | console.log('✅ Throws for null input'); 76 | passed++; 77 | } else { 78 | console.error('❌ Wrong error for null input:', error.message); 79 | failed++; 80 | } 81 | } 82 | 83 | // Test invalid input (non-object) 84 | try { 85 | await promiseStateAsync(123); 86 | console.error('❌ Should have thrown for number input'); 87 | failed++; 88 | } catch (error) { 89 | if (error.message === 'Expected a promise, got number') { 90 | console.log('✅ Throws for number input'); 91 | passed++; 92 | } else { 93 | console.error('❌ Wrong error for number input:', error.message); 94 | failed++; 95 | } 96 | } 97 | 98 | // Summary 99 | console.log('\n' + '='.repeat(50)); 100 | if (failed === 0) { 101 | console.log(`✅ All tests passed! (${passed}/${passed})`); 102 | } else { 103 | console.error(`❌ Some tests failed: ${passed} passed, ${failed} failed`); 104 | } 105 | })(); 106 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {test} from 'node:test'; 3 | import assert from 'node:assert/strict'; 4 | import delay from 'delay'; 5 | import {promiseStateAsync, promiseStateSync} from './index.js'; 6 | 7 | test('promiseStateAsync', async () => { 8 | const pendingPromise = new Promise(() => {}); 9 | const fulfilledPromise = Promise.resolve(); 10 | const rejectedPromise = Promise.reject(new Error('fixture')); 11 | 12 | assert.equal(await promiseStateAsync(pendingPromise), 'pending'); 13 | assert.equal(await promiseStateAsync(fulfilledPromise), 'fulfilled'); 14 | assert.equal(await promiseStateAsync(rejectedPromise), 'rejected'); 15 | 16 | rejectedPromise.catch(() => {}); // Prevent unhandled rejection 17 | }); 18 | 19 | test('promiseStateAsync - does not affect unhandled rejection', async () => { 20 | // Handle unhandled rejection temporarily for this test 21 | const originalHandler = process.listeners('unhandledRejection'); 22 | process.removeAllListeners('unhandledRejection'); 23 | 24 | let unhandledRejectionOccurred = false; 25 | const handler = () => { 26 | unhandledRejectionOccurred = true; 27 | }; 28 | 29 | process.once('unhandledRejection', handler); 30 | 31 | const fixture = Promise.reject(new Error('fixture')); 32 | await promiseStateAsync(fixture); 33 | 34 | // Wait for the event to fire 35 | await new Promise(resolve => { 36 | setTimeout(resolve, 10); 37 | }); 38 | 39 | assert.ok(unhandledRejectionOccurred, 'Unhandled rejection should have occurred'); 40 | 41 | // Clean up and restore original handlers 42 | fixture.catch(() => {}); 43 | for (const listener of originalHandler) { 44 | process.on('unhandledRejection', listener); 45 | } 46 | }); 47 | 48 | test('promiseStateAsync - state resolves before promise', async () => { 49 | const delayedPromise = delay(50); 50 | assert.equal(await promiseStateAsync(delayedPromise), 'pending'); 51 | await delayedPromise; 52 | assert.equal(await promiseStateAsync(delayedPromise), 'fulfilled'); 53 | }); 54 | 55 | test('promiseStateSync', () => { 56 | const pendingPromise = new Promise(() => {}); 57 | const fulfilledPromise = Promise.resolve(); 58 | const rejectedPromise = Promise.reject(); 59 | 60 | assert.equal(promiseStateSync(pendingPromise), 'pending'); 61 | assert.equal(promiseStateSync(fulfilledPromise), 'fulfilled'); 62 | assert.equal(promiseStateSync(rejectedPromise), 'rejected'); 63 | 64 | rejectedPromise.catch(() => {}); 65 | }); 66 | 67 | test('throws on non-promise input', async () => { 68 | await assert.rejects(promiseStateAsync(null), {message: 'Expected a promise, got object'}); 69 | await assert.rejects(promiseStateAsync(123), {message: 'Expected a promise, got number'}); 70 | await assert.rejects(promiseStateAsync('string'), {message: 'Expected a promise, got string'}); 71 | 72 | assert.throws(() => promiseStateSync(null), {message: 'Expected a promise, got object'}); 73 | assert.throws(() => promiseStateSync(123), {message: 'Expected a promise, got number'}); 74 | }); 75 | 76 | // Test for https://github.com/sindresorhus/p-state/issues/12 77 | test('promiseStateAsync - handles late rejections', async () => { 78 | // Handle unhandled rejection temporarily for this test 79 | const originalHandler = process.listeners('unhandledRejection'); 80 | process.removeAllListeners('unhandledRejection'); 81 | 82 | const unhandledRejections = []; 83 | const listener = error => unhandledRejections.push(error); 84 | process.on('unhandledRejection', listener); 85 | 86 | let rejectPromise; 87 | const lateRejectPromise = new Promise((_, reject) => { // eslint-disable-line promise/param-names 88 | rejectPromise = reject; 89 | }); 90 | 91 | assert.equal(await promiseStateAsync(lateRejectPromise), 'pending'); 92 | 93 | // Reject the promise after checking its state 94 | setTimeout(() => rejectPromise(new Error('Late rejection')), 50); 95 | 96 | await delay(100); 97 | 98 | assert.equal(unhandledRejections.length, 1); 99 | assert.equal(unhandledRejections[0]?.message, 'Late rejection'); 100 | 101 | // Clean up and restore original handlers 102 | lateRejectPromise.catch(() => {}); 103 | process.removeListener('unhandledRejection', listener); 104 | for (const listener of originalHandler) { 105 | process.on('unhandledRejection', listener); 106 | } 107 | }); 108 | --------------------------------------------------------------------------------